mirror of
https://github.com/LingCoder/OnJava8.git
synced 2026-08-23 22:43:28 +08:00
@@ -3,7 +3,7 @@
|
||||
<!-- Polymorphism -->
|
||||
# 第九章 多态
|
||||
|
||||
> 曾经有人请教我 ” Babbage 先生,如果输入错误的数字到机器中,会得出正确结果吗?“ 我无法理解产生如此问题的概念上的困惑。 —— Charles Babbage (1791 - 1871)
|
||||
> 曾经有人请教我 “ Babbage 先生,如果输入错误的数字到机器中,会得出正确结果吗?” 我无法理解产生如此问题的概念上的困惑。 —— Charles Babbage (1791 - 1871)
|
||||
|
||||
多态是面向对象编程语言中,继数据抽象和继承之外的第三个重要特性。
|
||||
|
||||
@@ -174,7 +174,7 @@ public static void tune(Instrument i) {
|
||||
|
||||
Java 中除了 **static** 和 **final** 方法(**private** 方法也是隐式的 **final**)外,其他所有方法都是后期绑定。这意味着通常情况下,我们不需要判断后期绑定是否会发生——它自动发生。
|
||||
|
||||
为什么将一个对象指明为 **final** ?正如前一章所述,它可以防止方法被覆写。但更重要的一点可能是,它有效地”关闭了“动态绑定,或者说告诉编译器不需要对其进行动态绑定。这可以让编译器为 **final** 方法生成更高效的代码。然而,大部分情况下这样做不会对程序的整体性能带来什么改变,因此最好是为了设计使用 **final**,而不是为了提升性能而使用。
|
||||
为什么将一个对象指明为 **final** ?正如前一章所述,它可以防止方法被重写。但更重要的一点可能是,它有效地”关闭了“动态绑定,或者说告诉编译器不需要对其进行动态绑定。这可以让编译器为 **final** 方法生成更高效的代码。然而,大部分情况下这样做不会对程序的整体性能带来什么改变,因此最好是为了设计使用 **final**,而不是为了提升性能而使用。
|
||||
|
||||
### 产生正确的行为
|
||||
|
||||
@@ -194,7 +194,7 @@ Shape s = new Circle();
|
||||
|
||||
这会创建一个 **Circle** 对象,引用被赋值给 **Shape** 类型的变量 s,这看似错误(将一种类型赋值给另一种类型),然而是没问题的,因此从继承上可认为圆(Circle)就是一个形状(Shape)。因此编译器认可了赋值语句,没有报错。
|
||||
|
||||
假设你调用了一个基类方法(在各个派生类中都被覆写):
|
||||
假设你调用了一个基类方法(在各个派生类中都被重写):
|
||||
|
||||
```java
|
||||
s.draw()
|
||||
@@ -214,7 +214,7 @@ public class Shape {
|
||||
}
|
||||
```
|
||||
|
||||
派生类通过覆写这些方法为每个具体的形状提供独一无二的方法行为:
|
||||
派生类通过重写这些方法为每个具体的形状提供独一无二的方法行为:
|
||||
|
||||
```java
|
||||
// polymorphism/shape/Circle.java
|
||||
@@ -469,7 +469,7 @@ Woodwind.play() MIDDLE_C
|
||||
|
||||
`tune()` 方法可以忽略周围所有代码发生的变化,仍然可以正常运行。这正是我们期待多态能提供的特性。代码中的修改不会破坏程序中其他不应受到影响的部分。换句话说,多态是一项“将改变的事物与不变的事物分离”的重要技术。
|
||||
|
||||
### 陷阱:”覆写“私有方法
|
||||
### 陷阱:“重写”私有方法
|
||||
|
||||
你可能天真地试图像下面这样做:
|
||||
|
||||
@@ -503,9 +503,9 @@ public Derived extends PrivateOverride {
|
||||
private f()
|
||||
```
|
||||
|
||||
你可能期望输出是 **public f()**,然而 **private** 方法也是 **final** 的,对于派生类来说是隐蔽的。因此,这里 **Derived** 的 `f()` 是一个全新的方法;因为基类版本的 `f()` 屏蔽了 **Derived** ,因此它都不算是重载方法。
|
||||
你可能期望输出是 **public f()**,然而 **private** 方法也是 **final** 的,对于派生类来说是隐蔽的。因此,这里 **Derived** 的 `f()` 是一个全新的方法;因为基类版本的 `f()` 屏蔽了 **Derived** ,因此它都不算是重写方法。
|
||||
|
||||
结论是只有非 **private** 方法才能被覆写,但是得小心覆写 **private** 方法的现象,编译器不报错,但不会按我们所预期的执行。为了清晰起见,派生类中的方法名采用与基类中 **private** 方法名不同的命名。
|
||||
结论是只有非 **private** 方法才能被重写,但是得小心重写 **private** 方法的现象,编译器不报错,但不会按我们所预期的执行。为了清晰起见,派生类中的方法名采用与基类中 **private** 方法名不同的命名。
|
||||
|
||||
如果使用了 `@Override` 注解,就能检测出问题:
|
||||
|
||||
@@ -735,7 +735,7 @@ Sandwich()
|
||||
|
||||
### 继承和清理
|
||||
|
||||
在使用组合和继承创建新类时,大部分时候你无需关心清理。子对象通常会留给垃圾收集器处理。如果你存在清理问题,那么必须用心地为新类创建一个 `dispose()` 方法(这里用的是我选择的名称,你可以使用更好的名称)。由于继承,如果有其他特殊的清理工作的话,就必须在派生类中覆写 `dispose()` 方法。当覆写 `dispose()` 方法时,记得调用基类的 `dispose()` 方法,否则基类的清理工作不会发生:
|
||||
在使用组合和继承创建新类时,大部分时候你无需关心清理。子对象通常会留给垃圾收集器处理。如果你存在清理问题,那么必须用心地为新类创建一个 `dispose()` 方法(这里用的是我选择的名称,你可以使用更好的名称)。由于继承,如果有其他特殊的清理工作的话,就必须在派生类中重写 `dispose()` 方法。当重写 `dispose()` 方法时,记得调用基类的 `dispose()` 方法,否则基类的清理工作不会发生:
|
||||
|
||||
```java
|
||||
// polymorphism/Frog.java
|
||||
@@ -972,7 +972,7 @@ Disposing Shared 0
|
||||
|
||||
在普通的方法中,动态绑定的调用是在运行时解析的,因为对象不知道它属于方法所在的类还是类的派生类。
|
||||
|
||||
如果在构造器中调用了动态绑定方法,就会用到那个方法的覆写定义。然而,调用的结果难以预料因为被覆写的方法在对象被完全构造出来之前已经被调用,这使得一些 bug 很隐蔽,难以发现。
|
||||
如果在构造器中调用了动态绑定方法,就会用到那个方法的重写定义。然而,调用的结果难以预料因为被重写的方法在对象被完全构造出来之前已经被调用,这使得一些 bug 很隐蔽,难以发现。
|
||||
|
||||
从概念上讲,构造器的工作就是创建对象(这并非是平常的工作)。在构造器内部,整个对象可能只是部分形成——只知道基类对象已经初始化。如果构造器只是构造对象过程中的一个步骤,且构造的对象所属的类是从构造器所属的类派生出的,那么派生部分在当前构造器被调用时还没有初始化。然而,一个动态绑定的方法调用向外深入到继承层次结构中,它可以调用派生类的方法。如果你在构造器中这么做,就可能调用一个方法,该方法操纵的成员可能还没有初始化——这肯定会带来灾难。
|
||||
|
||||
@@ -1024,12 +1024,12 @@ Glyph() after draw()
|
||||
RoundGlyph.RoundGlyph(), radius = 5
|
||||
```
|
||||
|
||||
**Glyph** 的 `draw()` 被设计为可覆写,在 **RoundGlyph** 这个方法被覆写。但是 **Glyph** 的构造器里调用了这个方法,结果调用了 **RoundGlyph** 的 `draw()` 方法,这看起来正是我们的目的。输出结果表明,当 **Glyph** 构造器调用了 `draw()` 时,**radius** 的值不是默认初始值 1 而是 0。这可能会导致在屏幕上只画了一个点或干脆什么都不画,于是我们只能干瞪眼,试图找到程序不工作的原因。
|
||||
**Glyph** 的 `draw()` 被设计为可重写,在 **RoundGlyph** 这个方法被重写。但是 **Glyph** 的构造器里调用了这个方法,结果调用了 **RoundGlyph** 的 `draw()` 方法,这看起来正是我们的目的。输出结果表明,当 **Glyph** 构造器调用了 `draw()` 时,**radius** 的值不是默认初始值 1 而是 0。这可能会导致在屏幕上只画了一个点或干脆什么都不画,于是我们只能干瞪眼,试图找到程序不工作的原因。
|
||||
|
||||
前一小节描述的初始化顺序并不十分完整,而这正是解决谜团的关键所在。初始化的实际过程是:
|
||||
|
||||
1. 在所有事发生前,分配给对象的存储空间会被初始化为二进制 0。
|
||||
2. 如前所述调用基类构造器。此时调用覆写后的 `draw()` 方法(是的,在调用 **RoundGraph** 构造器之前调用),由步骤 1 可知,**radius** 的值为 0。
|
||||
2. 如前所述调用基类构造器。此时调用重写后的 `draw()` 方法(是的,在调用 **RoundGraph** 构造器之前调用),由步骤 1 可知,**radius** 的值为 0。
|
||||
3. 按声明顺序初始化成员。
|
||||
4. 最终调用派生类的构造器。
|
||||
|
||||
@@ -1037,13 +1037,13 @@ RoundGlyph.RoundGlyph(), radius = 5
|
||||
|
||||
另一方面,应该震惊于输出结果。逻辑方面我们已经做得非常完美,然而行为仍不可思议的错了,编译器也没有报错(C++ 在这种情况下会产生更加合理的行为)。像这样的 bug 很容易被忽略,需要花很长时间才能发现。
|
||||
|
||||
因此,编写构造器有一条良好规范:做尽量少的事让对象进入良好状态。如果有可能的话,尽量不要调用类中的任何方法。在构造器中唯一能安全调用的只有基类的 **final** 方法(包括 **private** 方法,它们自动属于 **final**)。这些方法不能被覆写,因此不会产生意想不到的结果。你可能无法永远遵循这条规范,但应该朝着它努力。
|
||||
因此,编写构造器有一条良好规范:做尽量少的事让对象进入良好状态。如果有可能的话,尽量不要调用类中的任何方法。在构造器中唯一能安全调用的只有基类的 **final** 方法(包括 **private** 方法,它们自动属于 **final**)。这些方法不能被重写,因此不会产生意想不到的结果。你可能无法永远遵循这条规范,但应该朝着它努力。
|
||||
|
||||
<!-- Covariant Return Types -->
|
||||
|
||||
## 协变返回类型
|
||||
|
||||
Java 5 中引入了协变返回类型,这表示派生类的被覆写方法可以返回基类方法返回类型的派生类型:
|
||||
Java 5 中引入了协变返回类型,这表示派生类的被重写方法可以返回基类方法返回类型的派生类型:
|
||||
|
||||
```java
|
||||
// polymorphism/CovariantReturn.java
|
||||
@@ -1093,7 +1093,7 @@ Grain
|
||||
Wheat
|
||||
```
|
||||
|
||||
关键区别在于 Java 5 之前的版本强制要求被覆写的 `process()` 方法必须返回 **Grain** 而不是 **Wheat**,即使 **Wheat** 派生自 **Grain**,因而也应该是一种合法的返回类型。协变返回类型允许返回更具体的 **Wheat** 类型。
|
||||
关键区别在于 Java 5 之前的版本强制要求被重写的 `process()` 方法必须返回 **Grain** 而不是 **Wheat**,即使 **Wheat** 派生自 **Grain**,因而也应该是一种合法的返回类型。协变返回类型允许返回更具体的 **Wheat** 类型。
|
||||
|
||||
|
||||
<!-- Designing with Inheritance -->
|
||||
@@ -1160,7 +1160,7 @@ SadActor
|
||||
|
||||
### 替代 vs 扩展
|
||||
|
||||
采用“纯粹”的方式创建继承层次结构看上去是最清晰的方法。即只有基类的方法才能在派生类中被覆写,就像下图这样:
|
||||
采用“纯粹”的方式创建继承层次结构看上去是最清晰的方法。即只有基类的方法才能在派生类中被重写,就像下图这样:
|
||||
|
||||

|
||||
|
||||
|
||||
Reference in New Issue
Block a user