类再生
Think in Java 第六章 类再生,持续更新中,subscribe (。→‿←。) 酱
“Java引人注目的一项特性是代码的重复使用或者再生。但最具革命意义的是,除代码的复制和修改以外,我们还能做多得多的其他事情。”
在新类里简单地创建原有类的对象 new Car()。我们把这种方法叫作“合成”,因为新类由现有类的对象合并而成。
它创建一个新类,将其作为现有类的一个“类型”。我们可以原样采取现有类的形式,并在其中加入新代码,同时不会对现有的类产生影响。这种魔术般的行为叫作“继承”(Inheritance)
合成的语法
为进行合成,我们只需在新类里简单地置入对象句柄即可
//: SprinklerSystem.java
// Composition for code reuse
package c06;
class WaterSource {
private String s;
WaterSource() {
System.out.println("WaterSource()");
s = new String("Constructed");
}
public String toString() { return s; }
}
public class SprinklerSystem {
private String valve1, valve2, valve3, valve4;
WaterSource source; //对象句柄会初始化成null
int i;
float f;
void print() {
System.out.println("valve1 = " + valve1);
System.out.println("valve2 = " + valve2);
System.out.println("valve3 = " + valve3);
System.out.println("valve4 = " + valve4);
System.out.println("i = " + i);
System.out.println("f = " + f);
System.out.println("source = " + source);
}
public static void main(String[] args) {
SprinklerSystem x = new SprinklerSystem();
x.print();
}
} ///:~
希望句柄得到初始化,可在下面这些地方进行:
(1) 在对象定义的时候。这意味着它们在构建器调用之前肯定能得到初始化。
(2) 在那个类的构建器中(构造函数中)。
(3) 紧靠在要求实际使用那个对象之前。这样做可减少不必要的开销——假如对象并不需要创建的话。
对象句柄初始化 之前 调用 会报 空指针异常(常见的报错,NullPointException)
继承的语法
继承与Java(以及其他OOP语言:Oriented Object Programing)非常紧密地结合在一起。
关键字extends
// Detergent.java
// Inheritance syntax & properties
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 append()方法里,字串同一个s连接起来。这是用“+=”运算符实现的。同“+”一样,“+=”被Java用于对字串进行“过载”处理。
进行继承时,我们并不限于只能使用基础类的方法。亦可在衍生出来的类(子类)里加入自己的新方法。
初始化基础类
基础类及衍生类,而不再是以前的一个,所以在想象衍生类的结果对象时,可能会产生一些迷惑。从外部看,似乎新类拥有与基础类相同的接口,而且可包含一些额外的方法和字段。但继承并非仅仅简单地复制基础类的接口了事。创建衍生类的一个对象时,它在其中包含了基础类的一个“子对象”。这个子对象(指的是基类对象)就象我们根据基础类本身创建了它的一个对象。从外部看,基础类的子对象已封装到衍生类的对象里了。
当然,基础类子对象应该正确地初始化,而且只有一种方法能保证这一点:在构建器中执行初始化,通过调用基础类构建器,后者有足够的能力和权限来执行对基础类的初始化。在衍生类的构建器中,Java会自动插入对基础类构建器的调用。下面这个例子向大家展示了对这种三级继承的应用:
1、编译器也会为我们自动发出对基础类构建器(无参)的调用。
class Art {
Art() {
System.out.println("Art constructor");
}
}
class Drawing extends Art {
Drawing() {
System.out.println("Drawing constructor");
}
}
2、含有自变量的构建器
class Game {
Game(int i) {
System.out.println("Game constructor");
}
}
class BoardGame extends Game {
BoardGame(int i) {
super(i);
System.out.println("BoardGame constructor");
}
}
在衍生类构建器中,对基础类构建器的调用是必须做的第一件事情, super (i) ; 必须写在子类构造函数的第一行
确保正确的清除
在C++中,一旦破坏(清除)一个对象,就会自动调用破坏器方法。之所以将其省略,大概是由于在Java中只需简单地忘记对象,不需强行破坏它们。垃圾收集器会在必要的时候自动回收内存。
垃圾收集器大多数时候都能很好地工作,但在某些情况下,我们的类可能在自己的存在时期采取一些行动,而这些行动要求必须进行明确的清除工作。
我们并不知道垃圾收集器什么时候才会显身,或者说不知它何时会调用。所以一旦希望为一个类清除什么东西,必须写一个特别的方法,明确、专门地来做这件事情。同时,还要让客户程序员知道他们必须调用这个方法。
垃圾收集的顺序
不能指望自己能确切知道何时会开始垃圾收集。垃圾收集器可能永远不会得到调用(内存充足不会调)。 c++ 中有析构函数(对象清除前调用)
名字的隐藏
如果Java基础类有一个方法名被“过载”使用多次,在衍生类里对那个方法名的重新定义就不会隐藏任何基础类的版本。
很少会用与基础类里完全一致的签名和返回类型来覆盖同名的方法(子类覆盖基类,但是可以通过super.方法调用到基类被覆盖的方法)
到底选择合成还是继承
“属于”关系是用继承来表达的,而“包含”关系是用合成来表达的。
(类的关系 会在 UML课程 中类图绘制学习中有更深的理解)
继承是对一种特殊关系的表达,意味着“这个新类属于那个旧类的一种类型”。
在面向对象的程序设计中,创建和使用代码最可能采取的一种做法是:将数据和方法统一封装到一个类里,并且使用那个类的对象。有些时候,需通过“合成”技术用现成的类来构造新类。而继承是最少见的一种做法。
防止继承的滥用
final关键字
声明“这个东西不能改变”。
final关键字的三种应用场合:数据、方法以及类
final数据
常数主要应用于下述两个方面:
(1) 编译期常数,它永远不会改变
(2) 在运行期初始化的一个值,我们不希望它发生变化
对于基本数据类型,final会将值变成一个常数;但对于对象句柄,final会将句柄变成一个常数。进行声明时,必须将句柄初始化到一个具体的对象。而且永远不能将句柄变成指向另一个对象。
然而,对象本身是可以修改的。
空白final
允许我们创建“空白final”,它们属于一些特殊的字段。尽管被声明成final,但却未得到一个初始值。无论在哪种情况下,空白final都必须在实际使用前得到正确的初始化。
依然保持其“不变”的本质。
final方法
第一个是为方法“上锁”,防止任何继承类改变它的本来含义。设计程序时,若希望一个方法的行为在继承期间保持不变,而且不可被覆盖或改写,就可以采取这种做法。
final类
类肯定不需要进行任何改变;或者出于安全方面的理由,我们不希望进行子类化(子类处理)。不可被继承,String 类即是 final类
注意数据成员既可以是final,也可以不是,取决于我们具体选择。
将类定义成final后,结果只是禁止进行继承——没有更多的限制。然而,由于它禁止了继承,所以一个final类中的所有方法都默认为final。因为此时再也无法覆盖它们。
可为final类内的一个方法添加final指示符,但这样做没有任何意义。
常用的一个类是Vector。如果我们考虑代码的执行效率,就会发现只有不把任何方法设为final,才能使其发挥更大的作用。我们很容易就会想到自己应继承和覆盖如此有用的一个类,但它的设计者却否定了我们的想法。但我们至少可以用两个理由来反驳他们。首先,Stack(堆栈)是从Vector继承来的,亦即Stack“是”一个Vector,这种说法是不确切的。其次,对于Vector许多重要的方法,如addElement()以及elementAt()等,它们都变成了synchronized(同步的)。
final会造成显著的性能开销,可能会把final提供的性能改善抵销得一干二净。
Hashtable(散列表),它是另一个重要的标准类。该类没有采用任何final方法。
继承初始化
对整个初始化过程有所认识,其中包括继承
class Insect {
int i = 9;
int j;
Insect() {
prt("i = " + i + ", j = " + j);
j = 39;
}
static int x1 =
prt("static Insect.x1 initialized");
static int prt(String s) {
System.out.println(s);
return 47;
}
}
public class Beetle extends Insect {
int k = prt("Beetle.k initialized");
Beetle() {
prt("k = " + k);
prt("j = " + j);
}
static int x2 =
prt("static Beetle.x2 initialized");
static int prt(String s) {
System.out.println(s);
return 63;
}
public static void main(String[] args) {
prt("Beetle constructor");
Beetle b = new Beetle();
}
}
在装载过程中,装载程序注意它有一个基础类(即extends关键字要表达的意思),所以随之将其载入。无论是否准备生成那个基础类的一个对象,这个过程都会发生(请试着将对象的创建代码当作注释标注出来,自己去证实)。
若基础类含有另一个基础类,则另一个基础类随即也会载入,以此类推。接下来,会在根基础类(此时是Insect)执行static初始化,再在下一个衍生类执行,以此类推。保证这个顺序是非常关键的,因为衍生类的初始化可能要依赖于对基础类成员的正确初始化。
此时,必要的类已全部装载完毕,所以能够创建对象。首先,这个对象中的所有基本数据类型都会设成它们的默认值,而将对象句柄设为null。随后会调用基础类构建器。在这种情况下,调用是自动进行的。但也完全可以用super来自行指定构建器调用(就象在Beetle()构建器中的第一个操作一样)。基础类的构建采用与衍生类构建器完全相同的处理过程。基础顺构建器完成以后,实例变量会按本来的顺序得以初始化。最后,执行构建器剩余的主体部分。
总结
无论继承还是合成,我们都可以在现有类型的基础上创建一个新类型。但在典型情况下,我们通过合成来实现现有类型的“再生”或“重复使用”,将其作为新类型基础实施过程的一部分使用。但如果想实现接口的“再生”,就应使用继承。由于衍生或派生出来的类拥有基础类的接口,所以能够将其“上溯造型”为基础类。对于下一章要讲述的多形性问题,这一点是至关重要的。
练习
(1) 用默认构建器(空自变量列表)创建两个类:A和B,令它们自己声明自己。从A继承一个名为C的新类,并在C内创建一个成员B。不要为C创建一个构建器。创建类C的一个对象,并观察结果。
(2) 修改练习1,使A和B都有含有自变量的构建器,则不是采用默认构建器。为C写一个构建器,并在C的构建器中执行所有初始化工作。
(3) 使用文件Cartoon.java,将Cartoon类的构建器代码变成注释内容标注出去。解释会发生什么事情。
(4) 使用文件Chess.java,将Chess类的构建器代码作为注释标注出去。同样解释会发生什么。