第七章 复用类(上)
复用代码是Java众多引人注目的功能之一,但是想要成为极具革命性的语言,仅仅能够复制代码并对之加以改变是不够的,他还必须能够做更多的事情。
7.1 组合语法
我们到现在为止已经使用了多次组合计数,只需将对象引用置于新类中即可,例如:
class WaterSource { private String s; WaterSource () { System.out.println("WaterSource()"); s = "Constructed"; } public String toString() { return s; } } public class SprinklerSystem { private String value1, value2, value3, value4; private WaterSource source = new WaterSource(); private int i; private float f; public String toString() { return "value1 = " + value1 + " " + "value2 = " + value2 + " " + "value3 = " + value3 + " " + "value4 = " + value4 + " " + "i = " + i + " " + "f = " + f + " "+ "source = " + source; } public static void main(String[] args) { SprinklerSystem sprinklerSystem = new SprinklerSystem(); System.out.println(sprinklerSystem); } }
输出结果为:
WaterSource()
value1 = null value2 = null value3 = null value4 = null
i = 0 f = 0.0 source = Constructed
在上面定义的两个类的方法中,有一个很特殊:toString(),每个非基本类对象都有一个toString()方法,当编译器需要一个String而你只有一个对象时,该方法就会被调用。在这个表达式中:
“source = ” + source;
编译器将会得知你想要把一个String对象同WaterSource对象相加,由于只能将一个String对象和另一个String对象相加,因此编译器会调用toString(),把source转换成为一个String,再把这两个String链接到一起并将结果传递给System.out.println(),当想要使所创建的类具备这样的行为时,仅需要编写一个toString()方法即可。
编译器并不是简单地为每一个引用都创建默认对象,如果想初始化这些引用,可以在代码中的下列位置进行:
1、定义对象的地方
2、在类的构造器中
3、在使用这些对象之前(惰性初始化)
4、使用实例初始化
7.2 继承语法
继承是所有OOP语言和Java语言不可缺少的组成部分,当创建一个类时总是在继承,因此除非明确指出要从其他类中继承,否则就是隐式地从Object进行继承。在继承过程中,需要先声明新类与旧类相似,这种声明是通过在类主体的左边花括号之前,紧随基类名的关键字extends实现的,当这么做时,会自动得到基类中所有的域和方法。例如:
class Cleanser { private String s = "Cleanser"; public void append(String a) { s += a; } public void dilute() { append(" dilute()"); } public void apply() { append(" apply()"); } public void scrub() { append(" scrub()"); } public String toString() { return s; } public static void main(String[] args) { Cleanser x = new Cleanser(); x.dilute();x.apply();x.scrub(); System.out.println(x); } } public class Detergent extends Cleanser{ public void scrub() { append(" Detergent.scrub()"); super.scrub(); } public void foam() { append(" foam()"); } public static void main(String[] args) { Detergent x = new Detergent(); x.dilute();x.apply();x.scrub();x.foam(); System.out.println(x); System.out.println("Testing base class:"); Cleanser.main(args); } }
输出结果为:
Cleanser dilute() apply() Detergent.scrub() scrub() foam()
Testing base class:
Cleanser dilute() apply() scrub()
Cleanser和Detergent均含有main()方法,这种在每个类中都设置一个main()方法的的技术可以使每个类的单元测试都变得简便易行,而且在完成单元测试之后也无需删除main(),可以将其保留至下次测试。
Cleanser中所有的方法都必须是public的,为了继承,一般的额规则是将所有的数据成员都指定为private,将所有方法都指定为public,当然在特殊情况下可以做出调整,但这个方法是一个很有用的规则。
7.2.1 初始化基类
由于现在涉及基类和导出类两个类,所以要试着想象导出类所产生的结果对象。从外部来看,他就是一个与基类具有相同接口的新类,或许还会有一些额外的方法和域,但继承并不是复制基类的接口,当创建了一个导出类的对象时,该对象包含了一个基类的子对象,这个子对象与你用基类直接创建的对象时一样的,区别在于后者来自于外部,基类的子对象被包装在导出类的对象内部。
对基类子对象的正确初始化也很重要,在构造器中调用基类构造器来执行初始化,而基类构造器具有执行基类初始化所需要的所有知识和能力,Java会自动在导出类的构造器中插入对基类构造器的调用。例如:
class Art { Art() { System.out.println("Art constructor"); } } class Drawing extends Art { Drawing() { System.out.println("Drawing constructor"); } } public class Cartoon extends Drawing{ public Cartoon() { System.out.println("Cartoon constructor"); } public static void main(String[] args) { Cartoon x = new Cartoon(); } }
输出结果为:
Art constructor
Drawing constructor
Cartoon constructor
我们发现,构件过程是从基类向外扩散的,所以基类在导出类构造器可以访问它之前,就已经完成了初始化,即使你不为Cartoon()创建构造器,编译器也会为你合成一个默认的构造器,这里的构造器是调用基类的构造器。
带参数的构造器
上面这些例子各个类都含有默认的构造器,即这些构造器都不带参数,编译器可以轻松调用他们是因为不必考虑传递什么样的参数问题,但是如果没有默认的基类构造器,或者想调用一个带参数的基类构造器,就必须使用关键字super显式编写调用基类构造器的语句,并且配以适当的参数列表:
class Game { Game(int i) { System.out.println("Game constructor"); } } class BoardGame extends Game { BoardGame(int i) { super(i); System.out.println("Board constructor"); } } public class Chess extends BoardGame{ Chess() { super(1); System.out.println("Chess constructor"); } public static void main(String[] args) { Chess x = new Chess(); } }
输出结果如下:
Game constructor
Board constructor
Chess constructor
如果不是在BoardGame()中调用了基类构造器,编译器将无法找到符合Game()形式的构造器,电泳基类构造器必须是你在导出类构造器中要做的第一件事。
7.3 代理
第三种关系称为代理,Java没有提供对它的直接支持,这是继承于组合之间的中庸之道,我们将一个成员对象置于所要构造的类中,但与此同时我们在新类中暴露了该成员对象的所有方法。
7.4 组合使用组合和继承
同时使用组合和继承是很常见的事,下面这个例子就同时使用了这两项技术,并配以必要的构造器初始化来创建更加复杂的类:
class Plate { Plate(int i) { System.out.println("Plate constructor"); } } class DinnerPlate extends Plate { DinnerPlate(int i) { super(1); System.out.println("Dinner constructor"); } } class Utensil { Utensil(int i) { System.out.println("Utensil constructor"); } } class Spoon extends Utensil { Spoon(int i) { super(1); System.out.println("Spoon constuctor"); } } class Fork extends Utensil { Fork(int i) { super(1); System.out.println("Fork constructor"); } } class Knife extends Utensil { Knife(int i) { super(1); System.out.println("Knife constructor"); } } class Custom { Custom(int i) { System.out.println("Custom constructor"); } } public class PlaceSetting extends Custom{ private Spoon spoon; private Fork fork; private Knife knife; private DinnerPlate dinnerPlate; public PlaceSetting(int i) { super(i + 1); spoon = new Spoon(i+2); fork = new Fork(i+3); knife = new Knife(i+4); dinnerPlate = new DinnerPlate(i+5); System.out.println("PlaceSetting constructor"); } public static void main(String[] args){ PlaceSetting x = new PlaceSetting(9); } }
输出结果为:
Custom constructor
Utensil constructor
Spoon constuctor
Utensil constructor
Fork constructor
Utensil constructor
Knife constructor
Plate constructor
Dinner constructor
PlaceSetting constructor
7.4.1 确保正确清理
Java中没有C++中析构函数的概念,析构函数是一种在对象被销毁时可以自动调用的函数,其原因可能是因为在Java中我们的习惯只是忘掉而不是销毁对象,并且让垃圾回收器在必要时释放其内存。
通常这样是好事,但有时类可能要在其生命周期内执行一些必须执行的清理活动,你可能并不知道垃圾回收器什么时候会被调用,因此我们想要清理一些东西就必须显式地编写一个特殊方法来做这件事。
7.4.2 名称屏蔽
如果Java的基类拥有某个已经被多次重载的方法名称,那么在导出类中重新定义该方法名称并不会屏蔽其在基类中的任何版本,因此无论是在该层或者它的基类中对方法进行定义,重载机制都可以正常工作。
Java SE5新增加了@Override注解,它并不是关键字,但可以把它当成关键字使用,当你想要腹泻某个方法时,可以选择添加这个注解。
7.5 在组合与继承之间选择
组合和继承都允许在新的类中放置子对象,组合是显式地这样做,而继承则是隐式地做。组合技术通常用于想在新类中使用现有类的功能而非它的接口这种情形,也就是在新类中嵌入某个对象,让其实现所需要的功能,但新类的用户看到的只是为新类所定义的接口,而非嵌入对象的接口。有时,允许类的用户直接访问新类中的组合成分是极具意义的,也就是说将对象声明为public,如果对象自身都隐藏了具体实现,那么这种做法是安全的。
在继承的时候,使用某个现有类,并开发一个它的特殊版本,通常这意味着你在使用一个通用类,并为了某种特殊需要而将其特殊化。