第7章 多形性
上溯造型:将一个对象作为它自己的类型使用,或者作为它的基础类型的一个对象使用。取得一个对象句柄,并将其作为基础类型句柄使用。
方法调用的绑定:将一个方法调用同一个方法主体连接到一起就称为“绑定”(Binding)。若在程序运行以前执行绑定(由编译器和链接程序,如果有的话),就叫作“早期绑定”。大家以前或许从未听说过这个术语,因为它在任何程序化语言里都是不可能的。C 编译器只有一种方法调用,那就是“早期绑定”。
后期绑定(动态绑定、运行期绑定):绑定在运行期间进行,以对象的类型为基础。
Java 中绑定的所有方法都采用后期绑定技术,除非一个方法已被声明成 final。
将方法声明为final:防止其他人覆盖那个方法,可有效地“关闭”动态绑定,或者告诉编译器不需要进行动态绑定。这样一来,编译器就可为 final 方法调用生成效率更高的代码。
“过载”是指同一样东西在不同的地方具有多种含义;而“覆盖”是指它随时随地都只有一种含义,只是原先的含义完全被后来的含义取代了。
抽象类和方法
抽象方法:只含有一个声明,没有方法主体。abstract void X(); 包含了抽象方法的一个类叫作“抽象类”,类必须指定成abstract(抽象)。
若一个抽象类是不完整的,那么一旦有人试图生成那个类的一个对象,由于不能安全地为一个抽象类创建属于它的对象,所以会从编译器那里获得一条出错提示。通过这种方法,编译器可保证抽象类的“纯洁性”,我们不必担心会误用它。
如果从一个抽象类继承,而且想生成新类型的一个对象,就必须为基础类中的所有抽象方法提供方法定义。如果不这样做(完全可以选择不做),则衍生类也会是抽象的,而且编译器会强迫我们用 abstract 关键字标志那个类的“抽象”本质。
即使不包括任何 abstract 方法,亦可将一个类声明成“抽象类”。如果一个类没必要拥有任何抽象方法,而且我们想禁止那个类的所有实例,这种能力就会显得非常有用。
接口interface
“interface”(接口)关键字使抽象的概念更深入了一层。我们可将其想象为一个“纯”抽象类。它允许创建者规定一个类的基本形式:方法名、自变量列表以及返回类型,但不规定方法主体。接口也包含了基本数据类型的数据成员,但它们都默认为 static 和 final。接口只提供一种形式,并不提供实施的细节。
为创建一个接口,请使用 interface 关键字,而不要用 class。与类相似,我们可在interface 关键字的前面增加一个 public 关键字(但只有接口定义于同名的一个文件内);或者将其省略,营造一种“友好的”状态。 为了生成与一个特定的接口(或一组接口)相符的类,要使用implements(实现)关键字。我们要表达的意思是“接口看起来就象那个样子,这儿是它具体的工作细节”。
可将一个接口中的方法声明明确定义为“public”。但即便不明确定义,它们也会默认为 public。所以在实现一个接口的时候,来自接口的方法必须定义成 public。否则的话,它们会默认为“友好的”,而且会限制我们在继承过程中对一个方法的访问——Java 编译器不允许我们那样做。
Java的多重继承
接口只是比抽象类“更纯”的一种形式。它的用途并不止那些。由于接口根本没有具体的实施细节——也就是说,没有与存储空间与“接口”关联在一起——所以没有任何办法可以防止多个接口合并到一起。这一点是至关重要的,因为我们经常都需要表达这样一个意思:“x 从属于 a,也从属于 b,也从属于 c”。在 C++中,将多个类合并到一起的行动称作“多重继承”,而且操作较为不便,因为每个类都可能有一套自己的实施细节。在 Java 中,我们可采取同样的行动,但只有其中一个类拥有具体的实施细节。所以在合并多个接口的时候,C++的问题不会在 Java 中重演。
在一个衍生类中,我们并不一定要拥有一个抽象或具体(没有抽象方法)的基础类。如果确实想从一个非接口继承,那么只能从一个继承。剩余的所有基本元素都必须是“接口”。我们将所有接口名置于 implements关键字的后面,并用逗号分隔它们。可根据需要使用多个接口,而且每个接口都会成为一个独立的类型,可对其进行上溯造型。
通过继承扩展接口---利用继承技术,可方便地为一个接口添加新的方法声明,也可以将几个接口合并成一个新接口。
常数分组:由于置入一个接口的所有字段都自动具有 static 和 final 属性,所以接口是对常数值进行分组的一个好工具,它具有与 C 或 C++的 enum 非常相似的效果。
初始化接口中的字段:接口中定义的字段会自动具有 static 和 final 属性。它们不能是“空白 final”,但可初始化成非常数表达式。字段并不是接口的一部分,而是保存于那个接口的 static 存储区域中。
内部类:将一个类定义置入另一个类定义中。内部类对我们非常有用,因为利用它可对那些逻辑上相互联系的类进行分组,并可控制一个类在另一个类里的“可见性”。然而,我们必须认识到内部类与以前讲述的“合成”方法存在着根本的区别。
若想在除外部类非 static 方法内部之外的任何地方生成内部类的一个对象,必须将那个对象的类型设为“外部类名.内部类名”。
内部类和上溯造型
当我们准备上溯造型到一个基础类(特别是到一个接口)的时候,内部类开始发挥其关键作用(从用于实现的对象生成一个接口句柄具有与上溯造型至一个基础类相同的效果)。这是由于内部类随后可完全进入不可见或不可用状态——对任何人都将如此。所以我们可以非常方便地隐藏实施细节。我们得到的全部回报就是一个基础类或者接口的句柄,而且甚至有可能不知道准确的类型。
普通(非内部)类不可设为 private 或 protected——只允许 public 或者“友好的”。内部类可设为private或protected,限制外部类的访问。事实上,我们甚至不能下溯造型到一个 private 内部类(或者一个 protected 内部类,除非自己本身便是一个继承者),因为我们不能访问名字,就象在 classTest 里看到的那样。所以,利用 private 内部类,类设计人员可完全禁止其他人依赖类型编码,并可将具体的实施细节完全隐藏起来。除此以外,从客户程序员的角度来看,一个接口的范围没有意义的,因为他们不能访问不属于公共接口类的任何额外方法。这样一来,Java编译器也有机会生成效率更高的代码。
方法和作用域中的内部类
在一个方法甚至一个任意的作用域内创建内部类。有两方面的原因促使我们这样做:
(1) 正如前面展示的那样,我们准备实现某种形式的接口,使自己能创建和返回一个句柄。
(2) 要解决一个复杂的问题,并希望创建一个类,用来辅助自己的程序方案。同时不愿意把它公开。
方法和作用域中的内部类示例
interface Contents {
int value();
} public class Parcel4 {
public Destination dest(String s) {
class PDestination
implements Destination {
private String label;
private PDestination(String whereTo) {
label = whereTo;
}
public String readLabel() { return label; }
}
return new PDestination(s);
}
public static void main(String[] args) {
Parcel4 p = new Parcel4();
Destination d = p.dest("Tanzania");
}
}
PDestination 类属于 dest()的一部分,而不是 Parcel4 的一部分(同时注意可为相同目录内每个类内部的一个内部类使用类标识符 PDestination,这样做不会发生命名的冲突)。因此,PDestination 不可从 dest()的外部访问。请注意在返回语句中发生的上溯造型——除了指向基础类 Destination 的一个句柄之外,没有任何东西超出 dest()的边界之外。当然,不能由于类 PDestination 的名字置于 dest()内部,就认为在dest()返回之后 PDestination 不是一个有效的对象。
public class Parcel5 {
private void internalTracking(boolean b) {
if(b) {
class TrackingSlip {
private String id;
TrackingSlip(String s) {
id = s;
}
String getSlip() { return id; }
}
TrackingSlip ts = new TrackingSlip("slip");
String s = ts.getSlip();
}
// Can't use it here! Out of scope:
//! TrackingSlip ts = new TrackingSlip("x");
}
public void track() { internalTracking(true); }
public static void main(String[] args) {
Parcel5 p = new Parcel5();
p.track();
}
}
TrackingSlip 类嵌套于一个 if 语句的作用域内。这并不意味着类是有条件创建的——它会随同其他所有东西得到编译。然而,在定义它的那个作用域之外,它是不可使用的。除这些以外,它看起来和一个普通类并没有什么区别。
public class Parcel6 {
public Contents cont() {
return new Contents() {
private int i = 11;
public int value() { return i; }
}; // Semicolon required in this case
}
public static void main(String[] args) {
Parcel6 p = new Parcel6();
Contents c = p.cont();
}
}
创建从 Contents 衍生出来的匿名类的一个对象,由 new 表达式返回的句柄会自动上溯造型成一个 Contents 句柄。匿名内部类的语法其实要表达的是:
class MyContents extends Contents {
private int i = 11;
public int value() { return i; }
}
return new MyContents(); public class Wrapping {
private int i;
public Wrapping(int x){i = x;}
public int value(){return i;}
} public class Parcel7 {
public Wrapping wrap(int x) {
// Base constructor call:
return new Wrapping(x) {
public int value() {
return super.value() * 47;
}
}; // Semicolon required
}
public static void main(String[] args) {
Parcel7 p = new Parcel7();
Wrapping w = p.wrap(10);
}
}
我们将适当的自变量简单地传递给基础类构建器,在这儿表现为在“new Wrapping(x)”中传递x。匿名类不能拥有一个构建器,这和在调用 super()时的常规做法不同。
若试图定义一个匿名内部类,并想使用在匿名内部类外部定义的一个对象,则编译器要求外部对象为 final属性。这正是我们将 dest()的自变量设为 final 的原因。如果忘记这样做,就会得到一条编译期出错提示。
链接到外部类
创建自己的内部类时,那个类的对象同时拥有指向封装对象(这些对象封装或生成了内部类)的一个链接。所以它们能访问那个封装对象的成员——毋需取得任何资格。除此以外,内部类拥有对封装类所有元素的访问权限。
interface Selector {
boolean end();
Object current();
void next();
} public class Sequence {
private Object[] o;
private int next = 0;
public Sequence(int size) {
o = new Object[size];
}
public void add(Object x) {
if(next < o.length) {
o[next] = x;
next++;
}
}
private class SSelector implements Selector {
int i = 0;
public boolean end() {
return i == o.length;
}
public Object current() {
return o[i];
}
public void next() {
if(i < o.length) i++;
}
}
public Selector getSelector() {
return new SSelector();
}
public static void main(String[] args) {
Sequence s = new Sequence(10);
for(int i = 0; i < 10; i++)
s.add(Integer.toString(i));
Selector sl = s.getSelector();
while(!sl.end()) {
System.out.println((String)sl.current());
sl.next();
}
}
}
static内部类
为正确理解 static 在应用于内部类时的含义,必须记住内部类的对象默认持有创建它的那个封装类的一个对象的句柄。然而,假如我们说一个内部类是 static 的,这种说法却是不成立的。static 内部类意味着:
(1) 为创建一个 static 内部类的对象,我们不需要一个外部类对象。
(2) 不能从 static 内部类的一个对象中访问一个外部类对象。
但存在一些限制:由于 static 成员只能位于一个类的外部级别,所以内部类不可拥有 static 数据或static 内部类。 倘若为了创建内部类的对象而不需要创建外部类的一个对象,那么可将所有东西都设为 static。为了能正常工作,同时也必须将内部类设为 static。
public class Parcel10 {
private static class PContents
extends Contents {
private int i = 11;
public int value() { return i; }
}
protected static class PDestination
implements Destination {
private String label;
private PDestination(String whereTo) {
label = whereTo;
}
public String readLabel() { return label; }
}
public static Destination dest(String s) {
return new PDestination(s);
}
public static Contents cont() {
return new PContents();
}
public static void main(String[] args) {
Contents c = cont();
Destination d = dest("Tanzania");
}
}
通常,我们不在一个接口里设置任何代码,但 static 内部类可以成为接口的一部分。由于类是“静态”的,所以它不会违反接口的规则——static 内部类只位于接口的命名空间内部:
interface IInterface {
static class Inner {
int i, j, k;
public Inner() {}
void f() {}
}
}
可考虑用一个 static 内部类容纳自己的测试代码。这样便生成一个独立的、名为 TestBed¥Tester 的类(为运行程序,请使用“java TestBed$Tester”命令)。可将这个类用于测试,但不需在自己的最终发行版本中包含它。
class TestBed {
TestBed() {}
void f() { System.out.println("f()"); }
public static class Tester {
public static void main(String[] args) {
TestBed t = new TestBed();
t.f();
}
}
}
引用外部类对象
public class Parcel11 {
class Contents{
private int i = 11;
public int value(){return i;}
}
class Destination{
private String label;
Destination(String whereTo){label = whereTo;}
String readLabel(){return label;}
} public static void main(String[] args){
Parcel11 p = new Parcel11();
Parcel11.Contents c = p.new Contents();
Parcel11.Destination d = p.new Destination("inner use outer test");
}
}
为直接创建内部类的一个对象,不能象大家猜想的那样——采用相同的形式,并引用外部类名Parcel11。此时,必须利用外部类的一个对象生成内部类的一个对象: Parcel11.Contents c = p.new Contents();
因此,除非已拥有外部类的一个对象,否则不可能创建内部类的一个对象。这是由于内部类的对象已同创建它的外部的对象“默默”地连接到一起。然而,如果生成一个 static 内部类,就不需要指向外部类对象的一个句柄。
从内部类继承
由于内部类构建器必须同封装类对象的一个句柄联系到一起,所以从一个内部类继承的时候,情况会稍微变得有些复杂。这儿的问题是封装类的“秘密”句柄必须获得初始化,而且在衍生类中不再有一个默认的对象可以连接。解决这个问题的办法是采用一种特殊的语法,明确建立这种关联:
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);
}
}
从中可以看到,InheritInner 只对内部类进行了扩展,没有扩展外部类。但在需要创建一个构建器的时候,默认对象已经没有意义,我们不能只是传递封装对象的一个句柄。此外,必须在构建器中采用下述语法: enclosingClassHandle.super(); 它提供了必要的句柄,以便程序正确编译。
若创建一个内部类,然后从封装类继承,并重新定义内部类,不能覆盖内部类。
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();
}
} 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();
}
}
内部类标识符
由于每个类都会生成一个.class 文件,用于容纳与如何创建这个类型的对象有关的所有信息(这种信息产生了一个名为 Class 对象的元类),所以大家或许会猜到内部类也必须生成相应的.class 文件,用来容纳与它们的 Class 对象有关的信息。这些文件或类的名字遵守一种严格的形式:先是封装类的名字,再跟随一个$,再跟随内部类的名字。如果内部类是匿名的,那么编译器会简单地生成数字,把它们作为内部类标识符使用。若内部类嵌套于其他内部类中,则它们的名字简单地追加在一个¥以及外部类标识符的后面。
构建器和多形性
用于基础类的构建器肯定在一个衍生类的构建器中调用,而且逐渐向上链接,使每个基础类使用的构建器都能得到调用。之所以要这样做,是由于构建器负有一项特殊任务:检查对象是否得到了正确的构建。一个衍生类只能访问它自己的成员,不能访问基础类的成员(这些成员通常都具有 private 属性)。只有基础类的构建器在初始化自己的元素时才知道正确的方法以及拥有适当的权限。所以,必须令所有构建器都得到调用,否则整个对象的构建就可能不正确。那正是编译器为什么要强迫对衍生类的每个部分进行构建器调用的原因。在衍生类的构建器主体中,若我们没有明确指定对一个基础类构建器的调用,它就会“默默”地调用默认构建器。如果不存在默认构建器,编译器就会报告一个错误(若某个类没有构建器,编译器会自动组织一个默认构建器)。
构建器的调用顺序
构建器的调用遵照下面的顺序:
(1) 调用基础类构建器。这个步骤会不断重复下去,首先得到构建的是分级结构的根部,然后是下一个衍生类,等等。直到抵达最深一层的衍生类。
(2) 按声明顺序调用成员初始化模块。
(3) 调用衍生构建器的主体。
继承和finalize()
一般情况下JVM会负责类的成员对象的垃圾收集和处理,如果已经设计了某个特殊的清除进程,要求它必须作为垃圾收集的一部分进行,必须覆盖衍生类中的 finalize()方法。覆盖衍生类的 finalize()时,务必记住调用 finalize()的基础类版本。否则,基础类的初始化根本不会发生。
构建器内部的多形性方法的行为
若当前位于一个构建器的内部,同时调用准备构建的那个对象的一个动态绑定方法,会使用那个方法被覆盖的定义。然而,产生的效果可能并不如我们所愿,而且可能造成一些难于发现的程序错误。
abstract class Glyph {
abstract void draw();
Glyph() {
System.out.println("Glyph() before draw()");
draw();
System.out.println("Glyph() after draw()");
}
}
class RoundGlyph extends Glyph {
int radius = 1;
RoundGlyph(int r) {
radius = r;
System.out.println("RoundGlyph.RoundGlyph(), radius = " + radius);
}
void draw() {
System.out.println("RoundGlyph.draw(), radius = " + radius);
}
}
public class PolyConstructors {
public static void main(String[] args) {
new RoundGlyph(5);
}
}
输出结果:
Glyph() before draw()
RoundGlyph.draw(), radius = 0
Glyph() after draw()
RoundGlyph.RoundGlyph(), radius = 5
当 Glyph 的构建器调用 draw()时,radius 的值甚至不是默认的初始值 1,而是 0。前面讲述的初始化顺序并不十分完整,而那是解决问题的关键所在。初始化的实际过程是这样的:
(1) 在采取其他任何操作之前,为对象分配的存储空间初始化成二进制零。
(2) 就象前面叙述的那样,调用基础类构建器。此时,被覆盖的 draw()方法会得到调用(的确是在RoundGlyph 构建器调用之前),此时会发现 radius 的值为 0,这是由于步骤(1)造成的。
(3) 按照原先声明的顺序调用成员初始化代码。
(4) 调用衍生类构建器的主体。
采取这些操作要求有一个前提,那就是所有东西都至少要初始化成零(或者某些特殊数据类型与“零”等价的值),而不是仅仅留作垃圾。其中包括通过“合成”技术嵌入一个类内部的对象句柄。如果假若忘记初始化那个句柄,就会在运行期间出现违例事件。
设计构建器时一个特别有效的规则是:用尽可能简单的方法使对象进入就绪状态;如果可能,避免调用任何方法。在构建器内唯一能够安全调用的是在基础类中具有 final 属性的那些方法(也适用于 private方法,它们自动具有 final 属性)。这些方法不能被覆盖,所以不会出现上述潜在的问题。