类再生分为两种方式:
由于这儿涉及到两个类——基础类及衍生类,而不再是以前的一个,所以在想象衍生类的结果对象时,可能会产生一些迷惑。从外部看,似乎新类拥有与基础类相同的接口,而且可包含一些额外的方法和字段。但继承并非仅仅简单地复制基础类的接口了事。创建衍生类的一个对象时,它在其中包含了基础类的一个“子对象”。这个子对象就象我们根据基础类本身创建了它的一个对象。从外部看,基础类的子对象已封装到衍生类的对象里了。
当然,基础类子对象应该正确地初始化,而且只有一种方法能保证这一点:在构建器中执行初始化,通过调
用基础类构建器,后者有足够的能力和权限来执行对基础类的初始化。在衍生类的构建器中,Java 会自动插
入对基础类构建器的调用。下面这个例子向大家展示了对这种三级继承的应用:
public class Art { Art(){ System.out.println("art"); } } public class Drawing extends Art { Drawing(){ System.out.println("drawing"); } public static void main(String[] args) { Drawing drawing = new Drawing(); } }
输出结果为:
art
drawing
上述例子有自己默认的构建器;也就是说,它们不含任何自变量。编译器可以很容易地调用它们,因为不存
在具体传递什么自变量的问题。如果类没有默认的自变量,或者想调用含有一个自变量的某个基础类构建
器,必须明确地编写对基础类的调用代码。这是用 super 关键字以及适当的自变量列表实现的,如下所示:
class Game { Game(int i) { System.out.println("Game constructor"); } } class BoardGame extends Game { BoardGame(int i) { super(i); System.out.println("BoardGame constructor"); } } 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
尽管编译器会强迫我们对基础类进行初始化,并要求我们在构建器最开头做这一工作,但它并不会监视我们
是否正确初始化了成员对象。所以对此必须特别加以留意。
继承的一个好处是它支持“累积开发”,允许我们引入新的代码,同时不会为现有代码造成错误。这样可将
新错误隔离到新代码里。通过从一个现成的、功能性的类继承,同时增添成员新的数据成员及方法(并重新
定义现有方法),我们可保持现有代码原封不动(另外有人也许仍在使用它),不会为其引入自己的编程错
误。一旦出现错误,就知道它肯定是由于自己的新代码造成的。这样一来,与修改现有代码的主体相比,改
正错误所需的时间和精力就可以少很多。
继承最值得注意的地方就是它没有为新类提供方法。继承是对新类和基础类之间的关系的一种表达。可这样
总结该关系:“新类属于现有类的一种类型”。
这种表达并不仅仅是对继承的一种形象化解释,继承是直接由语言提供支持的。作为一个例子,大家可考虑
一个名为Instrument 的基础类,它用于表示乐器;另一个衍生类叫作Wind。由于继承意味着基础类的所有
方法亦可在衍生出来的类中使用,所以我们发给基础类的任何消息亦可发给衍生类。若Instrument 类有一个
play()方法,则Wind 设备也会有这个方法。这意味着我们能肯定地认为一个Wind 对象也是Instrument的一
种类型。下面这个例子揭示出编译器如何提供对这一概念的支持:
public class Instrument { public void play(){ System.out.println("hello"); } 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); } }
运行结果:
hello
这个例子中最有趣的无疑是tune()方法,它能接受一个Instrument句柄。但在 Wind.main()中,tune()方法
是通过为其赋予一个Wind 句柄来调用的。由于Java 对类型检查特别严格,所以大家可能会感到很奇怪,为
什么接收一种类型的方法也能接收另一种类型呢?但是,我们一定要认识到一个Wind 对象也是一个
Instrument对象。而且对于不在Wind 中的一个Instrument(乐器),没有方法可以由tune()调用。在
tune()中,代码适用于Instrument以及从 Instrument 衍生出来的任何东西。在这里,我们将从一个Wind 句
柄转换成一个Instrument 句柄的行为叫作“上溯造型”。
由于造型的方向是从衍生类到基础类,箭头朝上,所以通常把它叫作“上溯造型 ”,即Upcasting。上溯造
型肯定是安全的,因为我们是从一个更特殊的类型到一个更常规的类型。换言之,衍生类是基础类的一个超
集。它可以包含比基础类更多的方法,但它至少包含了基础类的方法。进行上溯造型的时候,类接口可能出
现的唯一一个问题是它可能丢失方法,而不是赢得这些方法。这便是在没有任何明确的造型或者其他特殊标
注的情况下,编译器为什么允许上溯造型的原因所在。
继承中对于final关键字的解释:
final int s = 1;//s为常数 final Object obj = new Object();//obj为不可变的句柄,但是obj内部变量可以变 final int s;//s为常量,但是使用前必须初始化 void method(final int s){}//当调用方法时,s得到一个值,但这个值在方法内只读
类装载顺序:
若基础类含有另一个基础类,则另一个基础类随即也会载入,以此类推。接下来,会在根基础类执行 static 初始化,再在下一个衍生类执行,以此类推。保证这个顺序是非常关键的,因为衍生类的初始化可能要依赖于对基础类成员的正确初始化。
总结:
无论继承还是合成,我们都可以在现有类型的基础上创建一个新类型。但在典型情况下,我们通过合成来实现现有类型的“再生”或“重复使用”,将其作为新类型基础实施过程的一部分使用。但如果想实现接口的“再生”,就应使用继承。由于衍生或派生出来的类拥有基础类的接口,所以能够将其“上溯造型”为基础类。对于下一章要讲述的多形性问题,这一点是至关重要的。
尽管继承在面向对象的程序设计中得到了特别的强调,但在实际启动一个设计时,最好还是先考虑采用合成技术。只有在特别必要的时候,才应考虑采用继承技术(下一章还会讲到这个问题)。合成显得更加灵活。但是,通过对自己的成员类型应用一些继承技巧,可在运行期准确改变那些成员对象的类型,由此可改变它们的行为。
尽管对于快速项目开发来说,通过合成和继承实现的代码再生具有很大的帮助作用。但在允许其他程序员完全依赖它之前,一般都希望能重新设计自己的类结构。我们理想的类结构应该是每个类都有自己特定的用途。它们不能过大(如集成的功能太多,则很难实现它的再生),也不能过小(造成不能由自己使用,或者不能增添新功能)。最终实现的类应该能够方便地再生。