第六章:代码复用
合成与继承
继承:
super关键字的使用:super使用在派生类中,如果派生类中重写了基类的方法,但在这个被重写的方法中仍然要调用基类的同名的方法,这就要用到super关键字,特别是在创建对象时,在带参数构造函数中调用基类构造函数的情况。
如:
class Cleanser {
private String s = new String("Cleanser");
public void append(String a) { s += a; }
public void dilute() { append(" dilute()"); }
public void apply() { append(" apply()"); }
public void scrub() { append(" scrub()"); }
public void print() { System.out.println(s); }
public static void main(String[] args) {
Cleanser x = new Cleanser();
x.dilute(); x.apply(); x.scrub();
x.print();
}
}
public class Detergent extends Cleanser {
// Change a method:
public void scrub() {
append(" Detergent.scrub()");
super.scrub(); // Call base-class version
}
// Add methods to the interface:
public void foam() { append(" foam()"); }
// Test the new class:
public static void main(String[] args) {
Detergent x = new Detergent();
x.dilute();
x.apply();
x.scrub();
x.foam();
x.print();
System.out.println("Testing base class:");
Cleanser.main(args);
}
} ///:~
可以看到基类Cleanser 中定义了scrub方法,但派生类Detergent 中对scrub方法进行了修改,并用在派生类Detergent 的scrub方法中,要调用基本的scrub方法,那么用super.scrub();
基类的初始化:
当你创建一个派生类的对象的时候,这个对象里面还有一个基类的子对象,这个子对象同基类自己创建的对象没什么两样,只是从外面看来,这个子对象被包裹在派生类的对象里面。
基类子对象的正确初始化是非常重要的,而且只有一个办法能保证这一点:调用基类的构造函数来进行初始化,因为只有它才能掌握怎么样才能正确地进行初始化的信息和权限。java会让派生类的构造函数自动地调用基类的构造函数。
示例:
class Art {
Art() {
System.out.println("Art constructor");
}
}
class Drawing extends Art {
Drawing() {
System.out.println("Drawing constructor");
}
}
public class Cartoon extends Drawing {
Cartoon() {
System.out.println("Cartoon constructor");
}
public static void main(String[] args) {
Cartoon x = new Cartoon();
}
} ///:~
输出结果为:
Art constructor
Drawing constructor
Cartoon constructor
一看结果便一目了然了。
上面的示例是不带任何参数的情况, 如果构造函数中带有参数的话,那这里又要用到super的特性了。与上面super的使用涵意一样,super在这里用作:派生的带参数构造函数中调用基类的带参构造函数,只是这里不象上面那样super.scrub();这里只使用super(i);即可。
class Game {
Game(int i) {
System.out.println("Game constructor");
}
}
class BoardGame extends Game {
BoardGame(int i) {
super(i);
System.out.println("BoardGame constructor");
}
}
public class Chess extends BoardGame {
Chess() {
super(11);
System.out.println("Chess constructor");
}
public static void main(String[] args) {
Chess x = new Chess();
}
} ///:~
输出结果是:
Game constructor
BoardGame constructor
Chess constructor
合成和继承一起使用,实现类的复用:
class Plate {
Plate(int i) {
System.out.println("Plate constructor");
}
}
class DinnerPlate extends Plate {
DinnerPlate(int i) {
super(i);
System.out.println(
"DinnerPlate constructor");
}
}
class Utensil {
Utensil(int i) {
System.out.println("Utensil constructor");
}
}
class Spoon extends Utensil {
Spoon(int i) {
super(i);
System.out.println("Spoon constructor");
}
}
class Fork extends Utensil {
Fork(int i) {
super(i);
System.out.println("Fork constructor");
}
}
class Knife extends Utensil {
Knife(int i) {
super(i);
System.out.println("Knife constructor");
}
}
// A cultural way of doing something:
class Custom {
Custom(int i) {
System.out.println("Custom constructor");
}
}
public class PlaceSetting extends Custom {
Spoon sp;
Fork frk;
Knife kn;
DinnerPlate pl;
PlaceSetting(int i) {//把初始化工作都放在构造函数中
super(i + 1);
sp = new Spoon(i + 2);
frk = new Fork(i + 3);
kn = new Knife(i + 4);
pl = new DinnerPlate(i + 5);
System.out.println(
"PlaceSetting constructor");
}
public static void main(String[] args) {
PlaceSetting x = new PlaceSetting(9);
}
} ///:~
尽管编译器会强迫我们对基础类进行初始化,并要求我们在构建器最开头做这一工作,但它并不会监视我们是否正确初始化了成员对象。所以对此必须特别加以留意。
FINAL关键字:
FINAL关键字指“那样东西是不允许改动”,你可能会出于两点考虑不想让别人作改动:设计和效率。由于这两个原因差别很大,所以很可能会误用final关键字。
final的三种用途:数据(Data)、方法(method)、类(class)。
很多语言通知编译器:“这段常量(constant)数据”的手段。常量能用下列两种情况出现:
1、可以是“编译时的常量”,这样以后就不能改了;
2、也可以是运行时初始化的值,这个值以后就不想再改了。
如果是编译时的常量,编译器会把常量放到算式里面;这样编译的时候就能进行计算,因此也就降低了运行时的开销。在Java 中这种常量必须是primitive 型的,而且要用final 关键词表示。这种常量的赋值必须在定义的时候进行。
一个既是static 又是final 的数据成员会只占据一段内存,并且不可修改。
当final 不是指primitive,而是用于对象的reference 的时候,意思就有点不一样了。对primitive 来说,final 会将这个值定义成常量,但是对于对象的reference 而言,final 的意思则是这个reference 是常量。初始化的时候,一旦将reference 连到了某个对象,那么它就再也不能指别的对象了。但是这个对象本身是可以修改的;Java 没有提供将某个对象作成常量的方法。
(但是你可以自己写一个类,这样就能把类当做常量了)
这种局限性也体现在数组上,因为它也是一个对象。
注意,通常约定,被初始化为常量值的final static 的primitive 的名字全都用大写,词与词之间用下
划线分开,如VAL_ONE
Final 方法
使用final 方法的目的有二:
第一,为方法上“锁”,禁止派生类进行修改。这是出于设计考虑。当你希望某个方法的功能,能在继承过程中被保留下来,并且不被覆写,就可以使用这个方法。
第二个原因就是效率。如果方法是final 的,那么编译器就会把调用转换成“内联的(inline)”。它会用方法本身的拷贝来代替方法的调用
final 和private
private 方法都隐含有final 的意思。由于你不能访问private 的方法,因此你也不能覆写它。你可以给private 方法加一个final 修饰符,但是这样做什么意义也没有。
这个问题有可能会造成混乱,因为即使你覆写了一个private 方法(它隐含有final 的意思),看上去它还是可以运行的,而且编译器也不会报错:
class WithFinals {
// Identical to "private" alone:
private final void f() {
System.out.println("WithFinals.f()");
}
/ / Also automatically "final":
private void g() {
System.out.println("WithFinals.g()");
}
}
class OverridingPrivate extends WithFinals {
private final void f() {
System.out.println("OverridingPrivate.f()");
}
private void g() {
System.out.println("OverridingPrivate.g()");
}
}
只有是基类接口里的东西才能被“覆写”,如果方法是private 的,那它就不属于基类的接口。它只能算是被类隐藏起来的,正好有着相同的名字的代码。如果你在派生类里创建了同名的public 或protected,或package 权限的方法,那么它们同基类中可能同名的方法,没有任何联系。你并没有覆写那个方法,你只是创建了一个新的方法。由于private 方法是无法访问的,实际上是看不见的,因此这么作除了会影响类的代码结构,其它什么意义都没有。
Final 类
把整个类都定义成final 的(把final 关键词放到类的定义部分的前面)就等于在宣布,你不会去继承这个类,你也不允许别人去继承这个类。换言之,出于类的设计考虑,它再也不需要作修改了,或者从安全角度出发,你不希望它再生出子类。
final class Dinosaur{}
注意,final 类的数据可以是final 的,也可以不是final 的,这要由你来决定。无论类是不是final 的,这一条都适用于“将final 用于数据的”场合。但是,由于final 类禁止了继承,覆写方法已经不可能了,因
此所有的方法都隐含地变成final 了。你可以为final 类的方法加一个final 修饰符,但是这一样没什么意义。