“Java引人注目的一项特性是代码的重复使用或者再生。但最具革命意义的是,除代码的复制和修改以外,我们还能做多得多的其他事情。”
在象C那样的程序化语言里,代码的重复使用早已可行,但效果不是特别显著。与Java的其他地方一样,这个方案解决的也是与类有关的问题。我们通过创建新类来重复使用代码,但却用不着重新创建,可以直接使用别人已建好并调试好的现成类。
但这样做必须保证不会干扰原有的代码。在这一章里,我们将介绍两个达到这一目标的方法。第一个最简单:在新类里简单地创建原有类的对象。我们把这种方法叫作“合成”,因为新类由现有类的对象合并而成。我们只是简单地重复利用代码的功能,而不是采用它的形式。
第二种方法则显得稍微有些技巧。它创建一个新类,将其作为现有类的一个“类型”。我们可以原样采取现有类的形式,并在其中加入新代码,同时不会对现有的类产生影响。这种魔术般的行为叫作“继承”(Inheritance),涉及的大多数工作都是由编译器完成的。对于面向对象的程序设计,“继承”是最重要的基础概念之一。它对我们下一章要讲述的内容会产生一些额外的影响。
对于合成与继承这两种方法,大多数语法和行为都是类似的(因为它们都要根据现有的类型生成新类型)。在本章,我们将深入学习这些代码再生或者重复使用的机制。
就以前的学习情况来看,事实上已进行了多次“合成”操作。为进行合成,我们只需在新类里简单地置入对象句柄即可。举个例子来说,假定需要在一个对象里容纳几个String对象、两种基本数据类型以及属于另一个类的一个对象。对于非基本类型的对象来说,只需将句柄置于新类即可;而对于基本数据类型来说,则需在自己的类中定义它们。如下所示(若执行该程序时有麻烦,请参见第3章3.1.2小节“赋值”):
//: 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; 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(); } } ///:~
WaterSource内定义的一个方法是比较特别的:toString()。大家不久就会知道,每种非基本类型的对象都有一个toString()方法。若编译器本来希望一个String,但却获得某个这样的对象,就会调用这个方法。所以在下面这个表达式中:
System.out.println("source = " + source) ;编译器会发现我们试图向一个WaterSource添加一个String对象("source
=")。这对它来说是不可接受的,因为我们只能将一个字串“添加”到另一个字串,所以它会说:“我要调用toString(),把source转换成字串!”经这样处理后,它就能编译两个字串,并将结果字串传递给一个System.out.println()。每次随同自己创建的一个类允许这种行为的时候,都只需要写一个toString()方法。
如果不深究,可能会草率地认为编译器会为上述代码中的每个句柄都自动构造对象(由于Java的安全和谨慎的形象)。例如,可能以为它会为WaterSource调用默认构建器,以便初始化source。打印语句的输出事实上是:
valve1 = null valve2 = null valve3 = null valve4 = null i = 0 f = 0.0 source = null
在类内作为字段使用的基本数据会初始化成零,就象第2章指出的那样。但对象句柄会初始化成null。而且假若试图为它们中的任何一个调用方法,就会产生一次“违例”。这种结果实际是相当好的(而且很有用),我们可在不丢弃一次违例的前提下,仍然把它们打印出来。
编译器并不只是为每个句柄创建一个默认对象,因为那样会在许多情况下招致不必要的开销。如希望句柄得到初始化,可在下面这些地方进行:
下面向大家展示了所有这三种方法:
//: Bath.java // Constructor initialization with composition class Soap { private String s; Soap() { System.out.println("Soap()"); s = new String("Constructed"); } public String toString() { return s; } } public class Bath { private String // Initializing at point of definition: s1 = new String("Happy"), s2 = "Happy", s3, s4; Soap castille; int i; float toy; Bath() { System.out.println("Inside Bath()"); s3 = new String("Joy"); i = 47; toy = 3.14f; castille = new Soap(); } void print() { // Delayed initialization: if(s4 == null) s4 = new String("Joy"); System.out.println("s1 = " + s1); System.out.println("s2 = " + s2); System.out.println("s3 = " + s3); System.out.println("s4 = " + s4); System.out.println("i = " + i); System.out.println("toy = " + toy); System.out.println("castille = " + castille); } public static void main(String[] args) { Bath b = new Bath(); b.print(); } } ///:~
请注意在Bath构建器中,在所有初始化开始之前执行了一个语句。如果不在定义时进行初始化,仍然不能保证能在将一条消息发给一个对象句柄之前会执行任何初始化——除非出现不可避免的运行期违例。
下面是该程序的输出:
Inside Bath() Soap() s1 = Happy s2 = Happy s3 = Joy s4 = Joy i = 47 toy = 3.14 castille = Constructed
调用print()时,它会填充s4,使所有字段在使用之前都获得正确的初始化。