第七章 复用类(下)
这里我就先直接将小节写在前边了,这样能帮助大家更快的获取到自己需要的信息。
小结:这一章讲的复用类主要有两种方式,一种是继承。另一种是组合。通常情况我们会看到这二者结合起来使用。在继承关系中,我们很容易将方法的重载和覆写区分开来,这里介绍了一个@Overread的关键字,当我们是覆写基类的方法时就加上这个关键字来区分,当然,在eclipse开发工具中会自动帮你添加的,平时只要注意观察一下就会看到的。这一章也非常详细的讲述了final关键字的作用以及使用的方法。特别是注意如何去使用final。另外这再次强调自己敲代码的好处,这里边的所有内容都是一个个写进去的,没有复制、粘贴。当对书中的内容不太理解时,就认真看一下代码,并亲手敲一遍代码,看运行的结果,这样能很有效的解决自己的疑惑。
7.4.1 确保正确清理
下面直接看代码然后尝试自己写出运行的结果就慢慢明白了。
class Shape{
Shape(int i){
System.out.println("Shape constructor");
}
void dispose(){
System.out.println("Shape dispose");
}
}
class Circle extends Shape{
Circle(int i) {
super(i);
System.out.println("Drawing Circle");
}
void dispose(){
System.out.println("Erasing Circle");
super.dispose();
}
}
class Triangle extends Shape{
Triangle(int i) {
super(i);
System.out.println("Drawing Triangle");
}
void dispose(){
System.out.println("Erasing Triangle");
super.dispose();
}
}
class Line extends Shape{
private int start,end;
Line(int start,int end){
super(start);
this.start=start;
this.end=end;
System.out.println("Drawing Line :"+start+","+end);
}
void dispose(){
System.out.println("Erasing Line :"+start+","+end);
super.dispose();
}
}
public class CADSystem extends Shape{
private Circle c;
private Triangle t;
private Line[] lines=new Line[3];
public CADSystem(int i){
super(i+1);
for(int j=0;j<lines.length;j++){
lines[j]=new Line(j,j*j);
}
c=new Circle(1);
t=new Triangle(1);
System.out.println("Combined constructor");
}
public void dispose(){
System.out.println("CADSystem dispose");
t.dispose();
c.dispose();
for(int i=lines.length-1;i>=0;i--){
lines[i].dispose();
}
super.dispose();
}
public static void main(String[] args) {
CADSystem x=new CADSystem(21);
try{
}finally{
x.dispose();
}
}
Shape(int i){
System.out.println("Shape constructor");
}
void dispose(){
System.out.println("Shape dispose");
}
}
class Circle extends Shape{
Circle(int i) {
super(i);
System.out.println("Drawing Circle");
}
void dispose(){
System.out.println("Erasing Circle");
super.dispose();
}
}
class Triangle extends Shape{
Triangle(int i) {
super(i);
System.out.println("Drawing Triangle");
}
void dispose(){
System.out.println("Erasing Triangle");
super.dispose();
}
}
class Line extends Shape{
private int start,end;
Line(int start,int end){
super(start);
this.start=start;
this.end=end;
System.out.println("Drawing Line :"+start+","+end);
}
void dispose(){
System.out.println("Erasing Line :"+start+","+end);
super.dispose();
}
}
public class CADSystem extends Shape{
private Circle c;
private Triangle t;
private Line[] lines=new Line[3];
public CADSystem(int i){
super(i+1);
for(int j=0;j<lines.length;j++){
lines[j]=new Line(j,j*j);
}
c=new Circle(1);
t=new Triangle(1);
System.out.println("Combined constructor");
}
public void dispose(){
System.out.println("CADSystem dispose");
t.dispose();
c.dispose();
for(int i=lines.length-1;i>=0;i--){
lines[i].dispose();
}
super.dispose();
}
public static void main(String[] args) {
CADSystem x=new CADSystem(21);
try{
}finally{
x.dispose();
}
}
}
运行结果:
Shape constructor
Shape constructor
Drawing Line :0,0
Shape constructor
Drawing Line :1,1
Shape constructor
Drawing Line :2,4
Shape constructor
Drawing Circle
Shape constructor
Drawing Triangle
Combined constructor
CADSystem dispose
Erasing Triangle
Shape dispose
Erasing Circle
Shape dispose
Erasing Line :2,4
Shape dispose
Erasing Line :1,1
Shape dispose
Erasing Line :0,0
Shape dispose
Shape constructor
Drawing Line :0,0
Shape constructor
Drawing Line :1,1
Shape constructor
Drawing Line :2,4
Shape constructor
Drawing Circle
Shape constructor
Drawing Triangle
Combined constructor
CADSystem dispose
Erasing Triangle
Shape dispose
Erasing Circle
Shape dispose
Erasing Line :2,4
Shape dispose
Erasing Line :1,1
Shape dispose
Erasing Line :0,0
Shape dispose
Shape dispose
7.4.2 名称屏蔽
如果Java的基类拥有某个已被多次重载的方法名称,那么在导出类中重新定义该方法名称并不会屏蔽其在基类中的任何版本(这一点与c++不同)。因此,无论是在该层或者它的基类中对方法进行定义,重载机制都可以正常工作:
class Homer{
char doh(char c){
System.out.println("doh(char c)");
return 'd';
}
float doh(float f){
System.out.println("doh(float f)");
return 1.0f;
}
}
class Milhouse{}
class Bart extends Homer{
void doh(Milhouse m){
System.out.println(" doh(Milhouse)");
}
}
public class Hide {
public static void main(String[] args) {
Bart b=new Bart();
b.doh(1);
b.doh('x');
b.doh(1.0f);
char doh(char c){
System.out.println("doh(char c)");
return 'd';
}
float doh(float f){
System.out.println("doh(float f)");
return 1.0f;
}
}
class Milhouse{}
class Bart extends Homer{
void doh(Milhouse m){
System.out.println(" doh(Milhouse)");
}
}
public class Hide {
public static void main(String[] args) {
Bart b=new Bart();
b.doh(1);
b.doh('x');
b.doh(1.0f);
b.doh(new Milhouse());
}
}
运行结果:
doh(float f)
doh(char c)
doh(float f)
doh(char c)
doh(float f)
doh(Milhouse)
区分重载和重写的最直接的判断是,看是否有@Override注解,有的表示重写。
7.5 在组合与继承之间选择
组合和继承都允许在新的类中放置子对象,组合是显示地这样做,而继承则是隐式地做。组合技术通常用于想在新类中使用现有类的功能而非它的接口这种情形。即,在新类中嵌入某个对象,让其实现所需要的功能,但新类用户看到的只是为新类所定义的接口,而非嵌入对象的接口。为取得此效果,需要在新类中嵌入一个现有类的private对象。
有时,允许类的用户直接访问新类中的组合部分是极具意义的;也就是说,将成员对象声明为public。如果成员对象自身都隐藏了具体实现,那么这种做法是安全的。当用户能够了解到你正在组织一组部件时,会使得端口更加易于理解。car对象就是一个很好的例子:
class Engine{
public void start(){}
public void rev(){}
public void stop(){}
}
class Wheel{
public void inflate(int psi){}
}
class Window{
public void roolup(){}
public void rooldown(){}
}
class Door{
public Window window=new Window();
public void open(){}
public void close(){}
}
public class Car {
public Engine engine=new Engine();
public Wheel[] wheel=new Wheel[4];
public Door
left=new Door(),
right=new Door();
public Car(){
for(int i=0;i<4;i++){
wheel[i]=new Wheel();
}
}
public static void main(String[] args) {
Car car=new Car();
car.left.window.roolup();
car.wheel[0].inflate(32);
}
public void start(){}
public void rev(){}
public void stop(){}
}
class Wheel{
public void inflate(int psi){}
}
class Window{
public void roolup(){}
public void rooldown(){}
}
class Door{
public Window window=new Window();
public void open(){}
public void close(){}
}
public class Car {
public Engine engine=new Engine();
public Wheel[] wheel=new Wheel[4];
public Door
left=new Door(),
right=new Door();
public Car(){
for(int i=0;i<4;i++){
wheel[i]=new Wheel();
}
}
public static void main(String[] args) {
Car car=new Car();
car.left.window.roolup();
car.wheel[0].inflate(32);
}
}
7.6 protected 关键字
现在,我们已经介绍完了继承,关键子protected最终具有了意义。在理想世界中,仅靠关键字private就已经足够了。但在实际项目中,经常会想要将某些事物尽可能对这个世界隐藏起来,但仍然允许导出类的成员访问它们。
关键字protected就是起这个作用的。它指明“就类用户而言,这是private的,但对于任何继承于此类的导出类或其他任何位于同一个包内的类来说,它却是可以访问的”。(protected也提供了包内访问权限)
7.7 向上转型
“为新的类提供方法”并不是继承技术中最重要的方面,其最重要的方面是用来表现新类和基类之间的关系。这种关系可以用“新类是现有类的一种类型”这句话加以概括。
class Instrument{
public void play(){}
static void tune(Instrument i){
i.play();
}
}
public class Wind extends Instrument{
public static void main(String[] args) {
Wind wind = new Wind();
Instrument.tune(wind);
static void tune(Instrument i){
i.play();
}
}
public class Wind extends Instrument{
public static void main(String[] args) {
Wind wind = new Wind();
Instrument.tune(wind);
}
}
在此例中,tune()方法可以接受Instrument引用,程序代码可以对Instrument和它所有的导出类起作用,这种将Wind引用转换为Instrument引用的动作,我们称之为向上转型。
7.7.1 为什么称为向上转型
该术语的使用有其历史原因,并且是以传统的类继承图的绘制方法为基础的:将根置于页面的顶端,然后逐渐向下。于是,Wind的继承图就是:
由导出类转型成基类,在继承图上是向上移动的,因此一般称为向上转型。由于向上转型是从一个较专用类型向较通用类型转换,所以总是很安全的。也就是说,导出类是基类的一个超集。
7.2.2 在论组合与继承
在面向对象编程中,生成和使用程序代码最有可能采用的方法就是直接将数据和方法包装进一个类中,并使用该类的对象。也可以运用组合技术使用现有类来开发新的类;而继承技术其实是不太常用的。因此,尽管在教授OOP的过程中我们多次强调继承,但这并不意味着要尽可能的使用它。相反,应当慎用这一技术,其使用场合仅限于你确信使用该技术确实有效的情况。到底是该用组合还是用继承,一个最清晰的判断办法就是问一问自己是否需要从新类向基类进行向上转型。如果必须是向上转型,则继承是必要的。
7.8 final关键字
根据上下文环境,Java的关键字final的含义存在着细微的区别,但通常它指的是“这是无法改变的。”不想做改变可能出于两种理由:设计或效率。由于这两个原因相差很远,所以关键字final有可能被误用。
以下几节谈论了可能使用到final的三种情况:数据、方法和类。
7.8.1 final数据
许多编程语言都有某种方法,来向编译器告知一块数据是恒定不变的。有时数据的恒定不变是很有用的,比如:
1、一个永不改变的编译时常量。
2、一个在运行时被初始化的值,而你不希望它被改变。
对于编译期常量这种情况,编译器可以将该常量值带入任何可能用到它的计算式中,也就是说,可以在编译时执行计算式,这减轻了一些运行时的负担。在Java中,这类常量必须是基本数据类型,并且以关键字final表示。在对这个常量进行定义的时候,必须对其进行赋值。
当对对象引用而不是基本类型运用final时,其含义会有一点令人迷惑。对应基本类型,final使数值恒定不变;而用于对象引用,final使引用恒定不变。一旦引用被初始化指向一个对象,就无法再把它改为指向另一个对象。然而,对象其自身却是可以被修改的,Java并未提供使任何对象恒定不变的途径(但可以自己编写类以取得使对象恒定不变的效果)。这一限制同样适用数组,它也是对象。
class Value{
int i;
public Value(int i){
this.i=i;
}
}
public class FinalData {
private static Random rand=new Random(47);
private String id;
public FinalData(String id){
this.id=id;
}
private final int valueOne=9;
private static final int VALUE_TWO=99;
public static final int VALUE_THREE=39;
public final int i4=rand.nextInt(20);
static final int INT_5=rand.nextInt(20);
private Value v1=new Value(11);
private final Value v2=new Value(22);
private static final Value VAL_3=new Value(33);
private final int[] a={1,2,3,4,5,6};
public String toString(){
return id+":"+"i4="+i4+", INT_5="+INT_5;
}
public static void main(String[] args) {
FinalData fd1 = new FinalData("fd1");
fd1.v2.i++;
fd1.v1=new Value(9);
for(int i=0;i<fd1.a.length;i++){
fd1.a[i]++;
}
System.out.println(fd1);
System.out.println("createing new finaldata");
FinalData fd2 = new FinalData("fd2");
System.out.println(fd1);
System.out.println(fd2);
}
int i;
public Value(int i){
this.i=i;
}
}
public class FinalData {
private static Random rand=new Random(47);
private String id;
public FinalData(String id){
this.id=id;
}
private final int valueOne=9;
private static final int VALUE_TWO=99;
public static final int VALUE_THREE=39;
public final int i4=rand.nextInt(20);
static final int INT_5=rand.nextInt(20);
private Value v1=new Value(11);
private final Value v2=new Value(22);
private static final Value VAL_3=new Value(33);
private final int[] a={1,2,3,4,5,6};
public String toString(){
return id+":"+"i4="+i4+", INT_5="+INT_5;
}
public static void main(String[] args) {
FinalData fd1 = new FinalData("fd1");
fd1.v2.i++;
fd1.v1=new Value(9);
for(int i=0;i<fd1.a.length;i++){
fd1.a[i]++;
}
System.out.println(fd1);
System.out.println("createing new finaldata");
FinalData fd2 = new FinalData("fd2");
System.out.println(fd1);
System.out.println(fd2);
}
}
运行结果:
fd1:i4=15, INT_5=18
createing new finaldata
fd1:i4=15, INT_5=18
createing new finaldata
fd1:i4=15, INT_5=18
fd2:i4=13, INT_5=18
这里我觉得主要是看最后两排结果,就是用final修饰的虽然对象的引用没有改变,但是对象可以改变,而用static修饰的只要被初始化了对象就再也不会被修改了。
空白final
Java允许生成“空白final”,所谓空白final是指被声明为final但又未给定初值的域。无论什么情况,编译器都确保空白final在使用前被初始化。但是,空白final的使用上提供了更大的灵活性,为此,一个类中的final域就可以做到根据对象而有所不同,却又保持恒定不变的特性。
class Poppet{
private int i;
Poppet(int ii){
this.i=ii;
}
}
public class BlankFinal {
private final int i=0;
private final int j;
private final Poppet p;
public BlankFinal(){
j=1;
p=new Poppet(1);
System.out.println("i="+i);
}
public BlankFinal(int x){
j=x;
p=new Poppet(x);
System.out.println("j="+j);
}
public static void main(String[] args) {
BlankFinal b=new BlankFinal();
new BlankFinal(23);
}
Poppet(int ii){
this.i=ii;
}
}
public class BlankFinal {
private final int i=0;
private final int j;
private final Poppet p;
public BlankFinal(){
j=1;
p=new Poppet(1);
System.out.println("i="+i);
}
public BlankFinal(int x){
j=x;
p=new Poppet(x);
System.out.println("j="+j);
}
public static void main(String[] args) {
BlankFinal b=new BlankFinal();
new BlankFinal(23);
}
}
必须在域的定义处或者每个构造器中用表达式对final进行赋值,这正是final域在使用前总是被初始化的原因所在。
注:这里再次强调一遍,如果自己去敲这段代码就明白上面说的是什么意思了,就是如果你不在构造器中给final赋值,那肯定就会报错。
final 参数
Java允许在参数列表中以声明的方式将参数指明为final。这意味着你无法在方法中更改参数引用所指向的对象:
class Gizmo{
public void spin(){}
}
public class FinalArguments {
void with(final Gizmo g){
// g=new Gizmo();
}
void without(Gizmo g){
g=new Gizmo();
g.spin();
}
// void f(final int i){
// i++;
// }
void f(int i){
i++;
}
int g(final int i){
return i+1;
}
public static void main(String[] args) {
FinalArguments bf = new FinalArguments();
bf.without(null);
bf.with(null);
}
public void spin(){}
}
public class FinalArguments {
void with(final Gizmo g){
// g=new Gizmo();
}
void without(Gizmo g){
g=new Gizmo();
g.spin();
}
// void f(final int i){
// i++;
// }
void f(int i){
i++;
}
int g(final int i){
return i+1;
}
public static void main(String[] args) {
FinalArguments bf = new FinalArguments();
bf.without(null);
bf.with(null);
}
}
方法f()和g()展示了当基本类型的参数被指明为final时所出现的结果:你可以读参数,但却无法修改参数。这一特性主要用来向匿名内部类传递数据,我们将在第十章中学习它。
7.8.2 final 方法
使用final方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义。这是出于设计考虑:要确保在继承中使方法行为保持不变,并且不会被覆盖。
过去建议使用final方法的第二个原因是效率。这里就没必要详细介绍具体效率了。不过现在是让编译器和JVM去处理效率问题,只有在想要明确禁止覆盖时,才将方法设置为final的。
final和private关键字
类中所有的private方法都隐式地指定为是final的。由于无法取用private方法,所以也就无法覆盖它。可以对private方法添加final修饰词,但这并不能给该方法增加任何额外的意义。
7.8.3 final类
当将某个类的整体定义为final时(通过将关键字final置于它的定义之前),就表明了你不打算继承该类,而且也不允许别人这样做。换句话说,出于某种考虑,你对该类的设计永不需要做任何变动,或者出于安全的考虑,你不希望它有子类。
7.8.4 有关final的忠告
在设计类时,将方法指明是final的,应该说是明智的。你可能会觉得,没人会想要覆盖你的方法。有时这是对的。
但请留意你所作的假设。要预见类是如何被复用的一般是很困难的,特别是对于一个通用类而言更是如此。如果将一个方法指定为final,可能会妨碍其他程序员在项目中通过继承类复用你的类,而这只是因为你没想到它会以那种方式被运用。
class Insect {
private int i=9;
protected int j;
Insect(){
System.out.println("i="+i+", j="+j);
j=39;
}
private static int x1=printInit("static insect.x1 initilized");
static int printInit(String s){
System.out.println(s);
return 47;
}
}
public class Beetle extends Insect{
private int k=printInit("Beetle.k initilized");
public Beetle(){
System.out.println("k="+k);
System.out.println("j="+j);
}
private static int x2=printInit("static insect.x2 initilized");
public static void main(String[] args) {
System.out.println("Beetle contructor");
new Beetle();
}
private int i=9;
protected int j;
Insect(){
System.out.println("i="+i+", j="+j);
j=39;
}
private static int x1=printInit("static insect.x1 initilized");
static int printInit(String s){
System.out.println(s);
return 47;
}
}
public class Beetle extends Insect{
private int k=printInit("Beetle.k initilized");
public Beetle(){
System.out.println("k="+k);
System.out.println("j="+j);
}
private static int x2=printInit("static insect.x2 initilized");
public static void main(String[] args) {
System.out.println("Beetle contructor");
new Beetle();
}
}
运行结果:
static insect.x1 initilized
static insect.x2 initilized
Beetle contructor
i=9, j=0
Beetle.k initilized
k=47
static insect.x2 initilized
Beetle contructor
i=9, j=0
Beetle.k initilized
k=47
j=39