# 7.2 深入理解
对于`Music.java`的困难性,可通过运行程序加以体会。输出是`Wind.play()`。这当然是我们希望的输出,但它看起来似乎并不愿按我们的希望行事。请观察一下`tune()`方法:
```
public static void tune(Instrument i) {
// ...
i.play(Note.middleC);
}
```
它接收`Instrument`引用。所以在这种情况下,编译器怎样才能知道`Instrument`引用指向的是一个`Wind`,而不是一个`Brass`或`Stringed`呢?编译器无从得知。为了深入了理解这个问题,我们有必要探讨一下“绑定”这个主题。
## 7.2.1 方法调用的绑定
将一个方法调用同一个方法主体连接到一起就称为“绑定”(Binding)。若在程序运行以前执行绑定(由编译器和链接程序,如果有的话),就叫作“早期绑定”。大家以前或许从未听说过这个术语,因为它在任何程序化语言里都是不可能的。C编译器只有一种方法调用,那就是“早期绑定”。
上述程序最令人迷惑不解的地方全与早期绑定有关,因为在只有一个`Instrument`引用的前提下,编译器不知道具体该调用哪个方法。
解决的方法就是“后期绑定”,它意味着绑定在运行期间进行,以对象的类型为基础。后期绑定也叫作“动态绑定”或“运行期绑定”。若一种语言实现了后期绑定,同时必须提供一些机制,可在运行期间判断对象的类型,并分别调用适当的方法。也就是说,编译器此时依然不知道对象的类型,但方法调用机制能自己去调查,找到正确的方法主体。不同的语言对后期绑定的实现方法是有所区别的。但我们至少可以这样认为:它们都要在对象中安插某些特殊类型的信息。
Java中绑定的所有方法都采用后期绑定技术,除非一个方法已被声明成`final`。这意味着我们通常不必决定是否应进行后期绑定——它是自动发生的。
为什么要把一个方法声明成`final`呢?正如上一章指出的那样,它能防止其他人覆盖那个方法。但也许更重要的一点是,它可有效地“关闭”动态绑定,或者告诉编译器不需要进行动态绑定。这样一来,编译器就可为`final`方法调用生成效率更高的代码。
## 7.2.2 产生正确的行为
知道Java里绑定的所有方法都通过后期绑定具有多态性以后,就可以相应地编写自己的代码,令其与基类沟通。此时,所有的派生类都保证能用相同的代码正常地工作。或者换用另一种方法,我们可以“将一条消息发给一个对象,让对象自行判断要做什么事情。”
在面向对象的程序设计中,有一个经典的“形状”例子。由于它很容易用可视化的形式表现出来,所以经常都用它说明问题。但很不幸的是,它可能误导初学者认为OOP只是为图形化编程设计的,这种认识当然是错误的。
形状例子有一个基类,名为`Shape`;另外还有大量派生类型:`Circle`(圆形),`Square`(方形),`Triangle`(三角形)等等。大家之所以喜欢这个例子,因为很容易理解“圆属于形状的一种类型”等概念。下面这幅继承图向我们展示了它们的关系:
![](https://box.kancloud.cn/0859fb072a3f03cb9e8086854ea7528c_424x214.gif)
向上转换可用下面这个语句简单地表现出来:
```
Shape s = new Circle();
```
在这里,我们创建了`Circle`对象,并将结果引用立即赋给一个`Shape`。这表面看起来似乎属于错误操作(将一种类型分配给另一个),但实际是完全可行的——因为按照继承关系,`Circle`属于`Shape`的一种。因此编译器认可上述语句,不会向我们提示一条出错消息。
当我们调用其中一个基类方法时(已在派生类里覆盖):
```
s.draw();
```
同样地,大家也许认为会调用`Shape`的`draw()`,因为这毕竟是一个`Shape`引用。那么编译器怎样才能知道该做其他任何事情呢?但此时实际调用的是`Circle.draw()`,因为后期绑定已经介入(多态性)。
下面这个例子从一个稍微不同的角度说明了问题:
```
//: Shapes.java
// Polymorphism in Java
class Shape {
void draw() {}
void erase() {}
}
class Circle extends Shape {
void draw() {
System.out.println("Circle.draw()");
}
void erase() {
System.out.println("Circle.erase()");
}
}
class Square extends Shape {
void draw() {
System.out.println("Square.draw()");
}
void erase() {
System.out.println("Square.erase()");
}
}
class Triangle extends Shape {
void draw() {
System.out.println("Triangle.draw()");
}
void erase() {
System.out.println("Triangle.erase()");
}
}
public class Shapes {
public static Shape randShape() {
switch((int)(Math.random() * 3)) {
default: // To quiet the compiler
case 0: return new Circle();
case 1: return new Square();
case 2: return new Triangle();
}
}
public static void main(String[] args) {
Shape[] s = new Shape[9];
// Fill up the array with shapes:
for(int i = 0; i < s.length; i++)
s[i] = randShape();
// Make polymorphic method calls:
for(int i = 0; i < s.length; i++)
s[i].draw();
}
} ///:~
```
针对从`Shape`派生出来的所有东西,`Shape`建立了一个通用接口——也就是说,所有(几何)形状都可以描绘和删除。派生类覆盖了这些定义,为每种特殊类型的几何形状都提供了独一无二的行为。
在主类`Shapes`里,包含了一个`static`方法,名为`randShape()`。它的作用是在每次调用它时为某个随机选择的`Shape`对象生成一个引用。请注意向上转换是在每个`return`语句里发生的。这个语句取得指向一个`Circle`,`Square`或者`Triangle`的引用,并将其作为返回类型`Shape`发给方法。所以无论什么时候调用这个方法,就绝对没机会了解它的具体类型到底是什么,因为肯定会获得一个单纯的`Shape`引用。
`main()`包含了`Shape`引用的一个数组,其中的数据通过对`randShape()`的调用填入。在这个时候,我们知道自己拥有`Shape`,但不知除此之外任何具体的情况(编译器同样不知)。然而,当我们在这个数组里步进,并为每个元素调用`draw()`的时候,与各类型有关的正确行为会魔术般地发生,就象下面这个输出示例展示的那样:
```
Circle.draw()
Triangle.draw()
Circle.draw()
Circle.draw()
Circle.draw()
Square.draw()
Triangle.draw()
Square.draw()
Square.draw()
```
当然,由于几何形状是每次随机选择的,所以每次运行都可能有不同的结果。之所以要突出形状的随机选择,是为了让大家深刻体会这一点:为了在编译的时候发出正确的调用,编译器毋需获得任何特殊的情报。对`draw()`的所有调用都是通过动态绑定进行的。
## 7.2.3 扩展性
现在,让我们仍然返回乐器(I`nstrument`)示例。由于存在多态性,所以可根据自己的需要向系统里加入任意多的新类型,同时毋需更改`true()`方法。在一个设计良好的OOP程序中,我们的大多数或者所有方法都会遵从`tune()`的模型,而且只与基类接口通信。我们说这样的程序具有“扩展性”,因为可以从通用的基类继承新的数据类型,从而新添一些功能。如果是为了适应新类的要求,那么对基类接口进行操纵的方法根本不需要改变,对于乐器例子,假设我们在基类里加入更多的方法,以及一系列新类,那么会出现什么情况呢?下面是示意图:
![](https://box.kancloud.cn/8a046c43b56c32f95c1f2b470fa7c805_364x400.gif)
所有这些新类都能与老类——`tune()`默契地工作,毋需对`tune()`作任何调整。即使`tune()`位于一个独立的文件里,而将新方法添加到`Instrument`的接口,`tune()`也能正确地工作,不需要重新编译。下面这个程序是对上述示意图的具体实现:
```
//: Music3.java
// An extensible program
import java.util.*;
class Instrument3 {
public void play() {
System.out.println("Instrument3.play()");
}
public String what() {
return "Instrument3";
}
public void adjust() {}
}
class Wind3 extends Instrument3 {
public void play() {
System.out.println("Wind3.play()");
}
public String what() { return "Wind3"; }
public void adjust() {}
}
class Percussion3 extends Instrument3 {
public void play() {
System.out.println("Percussion3.play()");
}
public String what() { return "Percussion3"; }
public void adjust() {}
}
class Stringed3 extends Instrument3 {
public void play() {
System.out.println("Stringed3.play()");
}
public String what() { return "Stringed3"; }
public void adjust() {}
}
class Brass3 extends Wind3 {
public void play() {
System.out.println("Brass3.play()");
}
public void adjust() {
System.out.println("Brass3.adjust()");
}
}
class Woodwind3 extends Wind3 {
public void play() {
System.out.println("Woodwind3.play()");
}
public String what() { return "Woodwind3"; }
}
public class Music3 {
// Doesn't care about type, so new types
// added to the system still work right:
static void tune(Instrument3 i) {
// ...
i.play();
}
static void tuneAll(Instrument3[] e) {
for(int i = 0; i < e.length; i++)
tune(e[i]);
}
public static void main(String[] args) {
Instrument3[] orchestra = new Instrument3[5];
int i = 0;
// Upcasting during addition to the array:
orchestra[i++] = new Wind3();
orchestra[i++] = new Percussion3();
orchestra[i++] = new Stringed3();
orchestra[i++] = new Brass3();
orchestra[i++] = new Woodwind3();
tuneAll(orchestra);
}
} ///:~
```
新方法是`what()`和`adjust()`。前者返回一个`String`引用,同时返回对那个类的说明;后者使我们能对每种乐器进行调整。
在`main()`中,当我们将某样东西置入`Instrument3`数组时,就会自动向上转换到`Instrument3`。
可以看到,在围绕`tune()`方法的其他所有代码都发生变化的同时,`tune()`方法却丝毫不受它们的影响,依然故我地正常工作。这正是利用多态性希望达到的目标。我们对代码进行修改后,不会对程序中不应受到影响的部分造成影响。此外,我们认为多态性是一种至关重要的技术,它允许程序员“将发生改变的东西同没有发生改变的东西区分开”。
- 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 推荐读物