第七章 多形性
面向对象的程序设计语言的基本特征:数据抽象、继承、多形性
上溯造型
取得一个对象句柄,并将其作为基础类型句柄使用的行为叫做“上溯造型”,因为继承树的画法是基础类位于最上方
从衍生类到基础类的上溯造型可能“缩小”接口,但不可能变得比基础类还要小
如果能不顾衍生类,只让自己的代码和基础类打交道,省下来的工作量将是难以估计的
原书示例:
1 | //: Music.java |
示例中,Music.tune()接收一个Instrument句柄,也接收从Instrument衍生出来的所有东西。如果让tune()简单地取得一个Wind句柄,将其作为自己的自变量使用,似乎会更加简单、直观得多,但如果这样做,每当增加一个衍生类时,就要再次为这个新衍生类构造一个tune()函数。
深入理解
- 对于上述的示例,假设Instrument有不止一个衍生类,比如Wind、Brass、String等,编译器怎样才能知道Instrment指向的是一个Wind而不是Brass或String,这里涉及到“绑定”
- 将一个方法调用同一个方法主体连接到一起就称为“绑定”(Binding)
- “早期绑定”:在程序运行以前执行绑定;“后期绑定”:绑定在运行期间进行,以对象的类型为基础,也叫做“动态绑定”或“运行期绑定”
- Java中绑定的所有方法都采用后期绑定技术,除非一个方法已被声明成final(防止其他人覆盖方法;“关闭”动态绑定)
覆盖(override)与过载(overload)
重写,也称为覆盖,是指派生类重写了基础类的方法;
重载,指一个类里面的方法名相同但是参数列表不同,编译器可以根据不同的参数来调用不同的方法实现重载
抽象类和抽象方法
- “抽象方法”属于一种不完整的方法,只含有一个声明,没有方法主体
- 包含了抽象方法的一个类叫做“抽象类”。如果一个类包含了一个或多个抽象方法,类就必须指定成abstract
- 抽象类的意图是为从它衍生出去的 所有类都创建一个通用接口
- 如果从一个抽象类继承,而且想生成新类型的一个对象,就必须为基础类中的所有抽象方法提供方法定义,否则衍生类也会是抽象的,而且编译器会强迫我们用abstract关键字标识它的“抽象”本质
- 即使不包括任何abstract 方法,亦可将一个类声明成“抽象类”。如果一个类没必要拥有任何抽象方法,而且我们想禁止那个类的所有实例
- 使一个类抽象以后,并不会强迫我们将它的所有方法都同时变成抽象
接口
接口(interface),可将其想象为一个“纯”抽象类,它允许创建者规定一个类的基本形式:方法名、自变量列表以及返回类型,但不规定方法主体
为了生成与一个特定的接口(或一组接口)相符的类,要使用 implements(实现)关键字,除此之外,其他工作都与继承极为相似
具体实现了一个借口后,就获得了一个普通的类,可用标准方式对其进行扩展
接口中的方法会被隐式的指定为 public abstract(只能是public abstract,其他修饰符会报错);接口中的变量会被隐式的指定为 public static final(只能是public,其他修饰符会报错)
可以上溯造型到一个接口,其会被认为是“普通类”
多重继承:可以从一个抽象或具体(没有抽象方法)的基础类继承,同时合并多个接口
原书示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32//: Adventure.java
// Multiple interfaces
import java.util.*;
interface CanFight {
void fight();
}
interface CanSwim {
void swim();
}
interface CanFly {
void fly();
}
class ActionCharacter {
public void fight() {}
}
class Hero extends ActionCharacter implements CanFight, CanSwim, CanFly {
public void swim() {}
public void fly() {}
}
public class Adventure {
static void t(CanFight x) { x.fight(); }
static void u(CanSwim x) { x.swim(); }
static void v(CanFly x) { x.fly(); }
static void w(ActionCharacter x) { x.fight(); }
public static void main(String[] args) {
Hero i = new Hero();
t(i); // Treat it as a CanFight
u(i); // Treat it as a CanSwim
v(i); // Treat it as a CanFly
w(i); // Treat it as an ActionCharacter
}
} ///:~从中可以看到,Hero将具体类ActionCharacter 同接口CanFight,CanSwim 以及CanFly 合并起来。按这种形式合并一个具体类与接口的时候,具体类必须首先出现,然后才是接口(否则编译器会报错)。在类Adventure 中,我们可看到共有四个方法,它们将不同的接口和具体类作为自己的自变量使用,Hero会依次上溯造型到每一个接口
使用接口的重要原因:能上溯造型至多个基础类;防止客户程序员制作这个类的一个对象,以及规定它仅仅是一个接口
如果事先知道某种东西会成为基础类,那么第一个选择就是把它变成一个接口。只有在必须使用方法定义或者成员变量的时候,才应考虑采用抽象类
利用继承技术,可方便地为一个接口添加新的方法声明,也可以将几个接口合并成一个新接口。通常,我们只能对单独一个类应用 extends(扩展)关键字,但由于接口可能由多个其他接口构成,所以在构建一个新接口时,extends 可能引用多个基础接口,多个接口之间用逗号隔开
接口中定义的字段会自动具有 static 和final 属性。虽然是final的,但可初始化成非常数表达式;由于字段是static的,所以它们会在首次装载类之后、以及首次访问任何字段之前获得初始化。字段并不是接口的一部分,而是保存在那个接口的static存储区域中
内部类
若想在除外部类非 static 方法内部之外的任何地方生成内部类的一个对象,必须将那个对象的类型设为“外部类名.内部类名”
由于内部类随后可完全进入不可见或不可用状态——对任何人都将如此,所以我们可以非常方便地隐藏实施细节。我们得到的全部回报就是一个基础类或者接口的句柄,而且甚至有可能不知道准确的类型
原书示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37//: Parcel3.java
// Returning a handle to an inner class
package c07.parcel3;
abstract class Contents {
abstract public int value();
}
interface Destination {
String readLabel();
}
public class Parcel3 {
private class PContents extends Contents {
private int i = 11;
public int value() { return i; }
}
protected class PDestination implements Destination {
private String label;
private PDestination(String whereTo) {
label = whereTo;
}
public String readLabel() { return label; }
}
public Destination dest(String s) {
return new PDestination(s);
}
public Contents cont() {
return new PContents();
}
}
class Test {
public static void main(String[] args) {
Parcel3 p = new Parcel3();
Contents c = p.cont();
Destination d = p.dest("Tanzania");
// Illegal -- can't access private class:
//! Parcel3.PContents c = p.new PContents();
}
} ///:~在Parcel3 中:内部类PContents 被设为 private,所以除了 Parcel3 之外,其他任何东西都不能访问它;PDestination 被设为 protected,所以除了 Parcel3,Parcel3 包内的类,以及Parcel3 的继承者之外,其他任何东西都不能访问 PDestination。这意味着客户程序员对这些成员的认识与访问将会受到限制
普通(非内部)类不可设为private 或 protected——只允许 public 或者“友好的”
使用内部类的原因:
(1) 我们准备实现某种形式的接口,使自己能创建和返回一个句柄;
(2) 要解决一个复杂的问题,并希望创建一个类,用来辅助自己的程序方案,同时不愿意把它公开
几种情况下的内部类:
(1)在一个方法内定义内部类:内部类是该方法的一部分,不能从方法外部访问
(2)在方法的一个作用域内定义的内部类:在定义这个内部类的作用域外,是不可访问它的
(3)匿名内部类:
若试图定义一个匿名内部类,并想使用在匿名内部类外部定义的一个对象,则编译器要求外部对象为final属性
原书示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25//: Destination.java
package c07.innerscopes;
interface Destination {
String readLabel();
} ///:~
//: Parcel8.java
// An anonymous inner class that performs
// initialization. A briefer version
// of Parcel5.java.
package c07.innerscopes;
public class Parcel8 {
// Argument must be final to use inside
// anonymous inner class:
public Destination dest(final String dest) {
return new Destination() {
private String label = dest;
public String readLabel() { return label; }
};
}
public static void main(String[] args) {
Parcel8 p = new Parcel8();
Destination d = p.dest("Tanzania");
}
} ///:~假如需要在匿名内部类中采取一些类似于构建器的行动,要使用实例初始化模块,即匿名内部类的构建器,不能对实例初始化模块进行重载处理
原书示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23//: Parcel9.java
// Using "instance initialization" to perform
// construction on an anonymous inner class
package c07.innerscopes;
public class Parcel9 {
public Destination dest(final String dest, final float price) {
return new Destination() {
private int cost;
// Instance initialization for each object:
{
cost = Math.round(price);
if(cost > 100)
System.out.println("Over budget!");
}
private String label = dest;
public String readLabel() { return label; }
};
}
public static void main(String[] args) {
Parcel9 p = new Parcel9();
Destination d = p.dest("Tanzania", 101.395F);
}
} ///:~一个内部类可以访问封装类的成员。内部类必须拥有对封装类的特定对象的一个引用,而封装类的作用就是创建这个内部类,随后,当我们引用封装类的一个成员时,就利用那个(隐藏)的引用来选择那个成(???)
内部类的对象默认持有创建它的那个封装类的一个对象的句柄
static 内部类意味着:
(1)为创建一个 static 内部类的对象,我们不需要一个外部类对象;
(2)不能从 static 内部类的一个对象中访问一个外部类对象
由于static 成员只能位于一个类的外部级别,所以内部类不可拥有static 数据或static 内部类
倘若为了创建内部类的对象而不需要创建外部类的一个对象,那么可将所有东西都设为static。为了能正常工作,同时也必须将内部类设为static。
我们不在一个接口里设置任何代码,但 static 内部类可以成为接口的一部分。由于类是“静态”的,所以它不会违反接口的规——static 内部类只位于接口的命名空间内部
引用外部类对象:必须利用外部类的一个对象生成内部类的一个对象。除非已拥有外部类的一个对象,否则不可能创建内部类的一个对象。然而,如果生成一个static 内部类,就不需要指向外部类对象的 一个句柄
从内部类继承:必须在构建器中采用下述语法:
enclosingClassHandle.super();
原书示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17//: InheritInner.java
// Inheriting an inner class
class WithInner {
class Inner {}
}
public class InheritInner extends WithInner.Inner {
//! InheritInner() {} // Won't compile
InheritInner(WithInner wi) {
wi.super();
}
public static void main(String[] args) {
WithInner wi = new WithInner();
InheritInner ii = new InheritInner(wi);
}
} ///:~内部类的覆盖:默认构建器是由编译器自动合成的,而且会调用基础类的默认构建器。当从外部类继承的时候,内部类不会被覆盖
原书示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27//: BigEgg.java
// An inner class cannot be overriden
// like a method
class Egg {
protected class Yolk {
public Yolk() {
System.out.println("Egg.Yolk()");
}
}
private Yolk y;
public Egg() {
System.out.println("New Egg()");
y = new Yolk();
}
}
public class BigEgg extends Egg {
public class Yolk {
public Yolk() {
System.out.println("BigEgg.Yolk()");
}
}
public static void main(String[] args) {
new BigEgg();
}
} ///:~结果:
1
2New Egg()
Egg.Yolk()如果“明确”地继承了内部类,且覆盖了它的方法,就会使用被覆盖的版本
原书示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35//: BigEgg2.java
// Proper inheritance of an inner class
class Egg2 {
protected class Yolk {
public Yolk() {
System.out.println("Egg2.Yolk()");
}
public void f() {
System.out.println("Egg2.Yolk.f()");
}
}
private Yolk y = new Yolk();
public Egg2() {
System.out.println("New Egg2()");
}
public void insertYolk(Yolk yy) { y = yy; }
public void g() { y.f(); }
}
public class BigEgg2 extends Egg2 {
public class Yolk extends Egg2.Yolk {
public Yolk() {
System.out.println("BigEgg2.Yolk()");
}
public void f() {
System.out.println("BigEgg2.Yolk.f()");
}
}
public BigEgg2() { insertYolk(new Yolk()); }
public static void main(String[] args) {
Egg2 e2 = new BigEgg2();
e2.g();
}
} ///:~结果:
1
2
3
4
5Egg2.Yolk()
New Egg2()
Egg2.Yolk()
BigEgg2.Yolk()
BigEgg2.Yolk.f()内部类的标识符:每个类都会生成一个.class文件,内部类也会生成.class文件,这些.class文件的命名遵循一种严格的形式:封装类名$内部类名.class,如由InheritInner.java文件就会生成三个.class文件:
InheritInner.class
WithInner$Inner.class
WithInner.class
构建器和多形性
基础类的构建器肯定在一个衍生类的构建器中调用,且逐渐向上链接,使每个类使用的构建器都能得到调用
对于一个复杂的对象,构建器的调用遵照如下顺序:
(1)调用基础类构建器。首先得到的是根部基础类的构建器,不断重复直到抵达最深一层的衍生类;
(2)按照声明顺序调用成员初始化模块;
(3)调用衍生类构建器的主体
如果在基础类中设计了某个特殊的清除进程,即finalize()方法,那么在衍生类中必须覆盖这个方法,覆盖finalize()时,务必调用基础类的finalize()方法
设计构建器时的规则:用尽可能简单的方法使对象进入就绪状态;如果可能,避免调用任何方法。在构建器内唯一安全的调用是final方法和private方法,因为这些方法不会被覆盖
通过继承进行设计
用继承表达行为间的差异,用成员变量表达状态的变化
纯继承与扩展:只有在基础类或接口中已建立的方法,才可以在衍生类中被覆盖,即衍生类与基础类拥有同样的接口,使用时无需知道子类的任何额外信息
“下溯造型”:在Java中,所有造型都会自动得到检查,即使是进行普通的括弧造型,进入运行期后,仍会对造型进行检查
“运行期类型标识”(RTTI):在运行期间对类型进行检查的行为