# 6.3 组合与继承的结合
许多时候都要求将组合与继承两种技术结合起来使用。下面这个例子展示了如何同时采用继承与组合技术,从而创建一个更复杂的类,同时进行必要的构造器初始化工作:
```
//: PlaceSetting.java
// Combining composition & inheritance
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);
}
} ///:~
```
尽管编译器会强迫我们对基类进行初始化,并要求我们在构造器最开头做这一工作,但它并不会监视我们是否正确初始化了成员对象。所以对此必须特别加以留意。
## 6.3.1 确保正确的清除
Java不具备象C++的“析构器”那样的概念。在C++中,一旦析构(清除)一个对象,就会自动调用析构器方法。之所以将其省略,大概是由于在Java中只需简单地忘记对象,不需强行析构它们。垃圾收集器会在必要的时候自动回收内存。
垃圾收集器大多数时候都能很好地工作,但在某些情况下,我们的类可能在自己的存在时期采取一些行动,而这些行动要求必须进行明确的清除工作。正如第4章已经指出的那样,我们并不知道垃圾收集器什么时候才会显身,或者说不知它何时会调用。所以一旦希望为一个类清除什么东西,必须写一个特别的方法,明确、专门地来做这件事情。同时,还要让客户程序员知道他们必须调用这个方法。而在所有这一切的后面,就如第9章(异常控制)要详细解释的那样,必须将这样的清除代码置于一个`finally`从句中,从而防范任何可能出现的异常事件。
下面介绍的是一个计算机辅助设计系统的例子,它能在屏幕上描绘图形:
```
//: CADSystem.java
// Ensuring proper cleanup
import java.util.*;
class Shape {
Shape(int i) {
System.out.println("Shape constructor");
}
void cleanup() {
System.out.println("Shape cleanup");
}
}
class Circle extends Shape {
Circle(int i) {
super(i);
System.out.println("Drawing a Circle");
}
void cleanup() {
System.out.println("Erasing a Circle");
super.cleanup();
}
}
class Triangle extends Shape {
Triangle(int i) {
super(i);
System.out.println("Drawing a Triangle");
}
void cleanup() {
System.out.println("Erasing a Triangle");
super.cleanup();
}
}
class Line extends Shape {
private int start, end;
Line(int start, int end) {
super(start);
this.start = start;
this.end = end;
System.out.println("Drawing a Line: " +
start + ", " + end);
}
void cleanup() {
System.out.println("Erasing a Line: " +
start + ", " + end);
super.cleanup();
}
}
public class CADSystem extends Shape {
private Circle c;
private Triangle t;
private Line[] lines = new Line[10];
CADSystem(int i) {
super(i + 1);
for(int j = 0; j < 10; j++)
lines[j] = new Line(j, j*j);
c = new Circle(1);
t = new Triangle(1);
System.out.println("Combined constructor");
}
void cleanup() {
System.out.println("CADSystem.cleanup()");
t.cleanup();
c.cleanup();
for(int i = 0; i < lines.length; i++)
lines[i].cleanup();
super.cleanup();
}
public static void main(String[] args) {
CADSystem x = new CADSystem(47);
try {
// Code and exception handling...
} finally {
x.cleanup();
}
}
} ///:~
```
这个系统中的所有东西都属于某种`Shape`(几何形状)。`Shape`本身是一种`Object`(对象),因为它是从根类明确继承的。每个类都重新定义了`Shape`的`cleanup()`方法,同时还要用`super`调用那个方法的基类版本。尽管对象存在期间调用的所有方法都可负责做一些要求清除的工作,但对于特定的`Shape`类——`Circle`(圆)、`Triangle`(三角形)以及`Line`(直线),它们都拥有自己的构造器,能完成“作图”(`draw`)任务。每个类都有它们自己的`cleanup()`方法,用于将非内存的东西恢复回对象存在之前的景象。
在`main()`中,可看到两个新关键字:`try`和`finally`。我们要到第9章才会向大家正式引荐它们。其中,`try`关键字指出后面跟随的块(由花括号定界)是一个“警戒区”。也就是说,它会受到特别的待遇。其中一种待遇就是:该警戒区后面跟随的`finally`从句的代码肯定会得以执行——不管`try`块到底存不存在(通过异常控制技术,`try`块可有多种不寻常的应用)。在这里,`finally`从句的意思是“总是为`x`调用`cleanup()`,无论会发生什么事情”。这些关键字将在第9章进行全面、完整的解释。
在自己的清除方法中,必须注意对基类以及成员对象清除方法的调用顺序——假若一个子对象要以另一个为基础。通常,应采取与C++编译器对它的“析构器”采取的同样的形式:首先完成与类有关的所有特殊工作(可能要求基类元素仍然可见),然后调用基类清除方法,就象这儿演示的那样。
许多情况下,清除可能并不是个问题;只需让垃圾收集器尽它的职责即可。但一旦必须由自己明确清除,就必须特别谨慎,并要求周全的考虑。
(1) 垃圾收集的顺序
不能指望自己能确切知道何时会开始垃圾收集。垃圾收集器可能永远不会得到调用。即使得到调用,它也可能以自己愿意的任何顺序回收对象。除此以外,Java 1.0实现的垃圾收集器机制通常不会调用`finalize()`方法。除内存的回收以外,其他任何东西都最好不要依赖垃圾收集器进行回收。若想明确地清除什么,请制作自己的清除方法,而且不要依赖`finalize()`。然而正如以前指出的那样,可强迫Java1.1调用所有收尾模块(`Finalizer`)。
## 6.3.2 名字的隐藏
只有C++程序员可能才会惊讶于名字的隐藏,因为它的工作原理与在C++里是完全不同的。如果Java基类有一个方法名被“重载”使用多次,在派生类里对那个方法名的重新定义就不会隐藏任何基类的版本。所以无论方法在这一级还是在一个基类中定义,重载都会生效:
```
//: Hide.java
// Overloading a base-class method name
// in a derived class does not hide the
// base-class versions
class Homer {
char doh(char c) {
System.out.println("doh(char)");
return 'd';
}
float doh(float f) {
System.out.println("doh(float)");
return 1.0f;
}
}
class Milhouse {}
class Bart extends Homer {
void doh(Milhouse m) {}
}
class Hide {
public static void main(String[] args) {
Bart b = new Bart();
b.doh(1); // doh(float) used
b.doh('x');
b.doh(1.0f);
b.doh(new Milhouse());
}
} ///:~
```
正如下一章会讲到的那样,很少会用与基类里完全一致的签名和返回类型来覆盖同名的方法,否则会使人感到迷惑(这正是C++不允许那样做的原因,所以能够防止产生一些不必要的错误)。
- Java 编程思想
- 写在前面的话
- 引言
- 第1章 对象入门
- 1.1 抽象的进步
- 1.2 对象的接口
- 1.3 实现方案的隐藏
- 1.4 方案的重复使用
- 1.5 继承:重新使用接口
- 1.6 多态对象的互换使用
- 1.7 对象的创建和存在时间
- 1.8 异常控制:解决错误
- 1.9 多线程
- 1.10 永久性
- 1.11 Java和因特网
- 1.12 分析和设计
- 1.13 Java还是C++
- 第2章 一切都是对象
- 2.1 用引用操纵对象
- 2.2 所有对象都必须创建
- 2.3 绝对不要清除对象
- 2.4 新建数据类型:类
- 2.5 方法、参数和返回值
- 2.6 构建Java程序
- 2.7 我们的第一个Java程序
- 2.8 注释和嵌入文档
- 2.9 编码样式
- 2.10 总结
- 2.11 练习
- 第3章 控制程序流程
- 3.1 使用Java运算符
- 3.2 执行控制
- 3.3 总结
- 3.4 练习
- 第4章 初始化和清除
- 4.1 用构造器自动初始化
- 4.2 方法重载
- 4.3 清除:收尾和垃圾收集
- 4.4 成员初始化
- 4.5 数组初始化
- 4.6 总结
- 4.7 练习
- 第5章 隐藏实现过程
- 5.1 包:库单元
- 5.2 Java访问指示符
- 5.3 接口与实现
- 5.4 类访问
- 5.5 总结
- 5.6 练习
- 第6章 类复用
- 6.1 组合的语法
- 6.2 继承的语法
- 6.3 组合与继承的结合
- 6.4 到底选择组合还是继承
- 6.5 protected
- 6.6 累积开发
- 6.7 向上转换
- 6.8 final关键字
- 6.9 初始化和类装载
- 6.10 总结
- 6.11 练习
- 第7章 多态性
- 7.1 向上转换
- 7.2 深入理解
- 7.3 覆盖与重载
- 7.4 抽象类和方法
- 7.5 接口
- 7.6 内部类
- 7.7 构造器和多态性
- 7.8 通过继承进行设计
- 7.9 总结
- 7.10 练习
- 第8章 对象的容纳
- 8.1 数组
- 8.2 集合
- 8.3 枚举器(迭代器)
- 8.4 集合的类型
- 8.5 排序
- 8.6 通用集合库
- 8.7 新集合
- 8.8 总结
- 8.9 练习
- 第9章 异常差错控制
- 9.1 基本异常
- 9.2 异常的捕获
- 9.3 标准Java异常
- 9.4 创建自己的异常
- 9.5 异常的限制
- 9.6 用finally清除
- 9.7 构造器
- 9.8 异常匹配
- 9.9 总结
- 9.10 练习
- 第10章 Java IO系统
- 10.1 输入和输出
- 10.2 增添属性和有用的接口
- 10.3 本身的缺陷:RandomAccessFile
- 10.4 File类
- 10.5 IO流的典型应用
- 10.6 StreamTokenizer
- 10.7 Java 1.1的IO流
- 10.8 压缩
- 10.9 对象序列化
- 10.10 总结
- 10.11 练习
- 第11章 运行期类型识别
- 11.1 对RTTI的需要
- 11.2 RTTI语法
- 11.3 反射:运行期类信息
- 11.4 总结
- 11.5 练习
- 第12章 传递和返回对象
- 12.1 传递引用
- 12.2 制作本地副本
- 12.3 克隆的控制
- 12.4 只读类
- 12.5 总结
- 12.6 练习
- 第13章 创建窗口和程序片
- 13.1 为何要用AWT?
- 13.2 基本程序片
- 13.3 制作按钮
- 13.4 捕获事件
- 13.5 文本字段
- 13.6 文本区域
- 13.7 标签
- 13.8 复选框
- 13.9 单选钮
- 13.10 下拉列表
- 13.11 列表框
- 13.12 布局的控制
- 13.13 action的替代品
- 13.14 程序片的局限
- 13.15 视窗化应用
- 13.16 新型AWT
- 13.17 Java 1.1用户接口API
- 13.18 可视编程和Beans
- 13.19 Swing入门
- 13.20 总结
- 13.21 练习
- 第14章 多线程
- 14.1 反应灵敏的用户界面
- 14.2 共享有限的资源
- 14.3 堵塞
- 14.4 优先级
- 14.5 回顾runnable
- 14.6 总结
- 14.7 练习
- 第15章 网络编程
- 15.1 机器的标识
- 15.2 套接字
- 15.3 服务多个客户
- 15.4 数据报
- 15.5 一个Web应用
- 15.6 Java与CGI的沟通
- 15.7 用JDBC连接数据库
- 15.8 远程方法
- 15.9 总结
- 15.10 练习
- 第16章 设计模式
- 16.1 模式的概念
- 16.2 观察器模式
- 16.3 模拟垃圾回收站
- 16.4 改进设计
- 16.5 抽象的应用
- 16.6 多重分发
- 16.7 访问器模式
- 16.8 RTTI真的有害吗
- 16.9 总结
- 16.10 练习
- 第17章 项目
- 17.1 文字处理
- 17.2 方法查找工具
- 17.3 复杂性理论
- 17.4 总结
- 17.5 练习
- 附录A 使用非JAVA代码
- 附录B 对比C++和Java
- 附录C Java编程规则
- 附录D 性能
- 附录E 关于垃圾收集的一些话
- 附录F 推荐读物