多态实现了“是什么”与“怎样做”两个模块的分离,代码的组织以及可读性均能获得改善。此外,还能创建“易于扩展”的程序。无论在项目的创建过程中,还是在需要加入新特性的时候,它们都可以方便地扩展。
多态旨在消除类型之间的耦合关系。我们知道,通过继承可将一个对象当作它自己的类型或者它自己的基础类型对待。这种能力是十分重要的,因为多个类型(只要从相同的基础类型中衍生出来)可被当作同一种类型对待。因而只需一段代码,即可对所有不同的类型进行同样的处理。而类型之间的区别,是根据方法行为的不同而表现出的,虽然这些方法均可通过对于基类的调用做统一处理。
1. 向上转型与多态
1.1 多态的机理
我们知道,对象可以作为它自己本身的类型使用,也可以作为它的基类使用,而这种把某种对象的引用视为对其基类的引用的做法被称为——向上转型。例如下面的代码,
Wind便是
Instrument类继承。
//: polymorphism/music/Instrument.java
package polymorphism.music;
import static net.mindview.util.Print.*;
class Instrument {
public void play(Note n) {
print("Instrument.play()");
}
}
///:~
//: polymorphism/music/Wind.java
package polymorphism.music;
// Wind objects are instruments
// because they have the same interface:
public class Wind extends Instrument {
// Redefine interface method:
public void play(Note n) {
System.out.println("Wind.play() " + n);
}
} ///:~
//: polymorphism/music/Music.java
// Inheritance & upcasting.
package polymorphism.music;
public class Music {
public static void tune(Instrument i) {
// ...
i.play(Note.MIDDLE_C);
}
public static void main(String[] args) {
Wind flute = new Wind();
tune(flute); // Upcasting
}
} /* Output:
Wind.play() MIDDLE_C
*///:~
Shape s = new Circle();
我们假定Circle已经继承了Shape,那么这么写是没有问题的。同时,JVM也会正确的理解为你创建了Circle类型的对象,你运行的是Circle.f(),而不是Shape.f()。
但是有这样一个问题,
tune()函数的输入时
Instrument类型,而不是
Wind。如果我们加入了类似
Brass或
Violin,那么我们需要为这些类型均实现一个不同的方法,这显然比较冗余。
因而我们需要一个简单的方法,它仅仅接受基类作为参数,编写的代码只和基类打交道。这就是多态存在的现实意义。
多态的解决方案:多态解决上述问题的方法是——后期绑定,或者成为“运行时绑定”。Java中出了final和static方法职位,其他的方法默认都是后期绑定,这个与C语言很不一样。而将方法设定为final的意义在于,防止其他人将这个方法覆盖,并关闭后期绑定(可小幅度提升性能)。那么如何实现多态呢?方法很简单,只要在导出类中重写一个实现,将基类中同样的方法覆盖即可。这个不再举例了。
如果新类型可以很好的适应已有的方法,并且只和基类的接口通信,那么我们称这种程序是可扩展的。
Note: 多态的覆盖只能覆盖public方法,而不能覆盖private方法。如果覆盖了,JVM不会报错,但是也不会按照覆盖后的进行运行。
如果新类型可以很好的适应已有的方法,并且只和基类的接口通信,那么我们称这种程序是可扩展的。
Note: 多态的覆盖只能覆盖public方法,而不能覆盖private方法。如果覆盖了,JVM不会报错,但是也不会按照覆盖后的进行运行。
1.2 多态的陷阱
我们可能会认为,所有事物都可以多态的发生。然而,
只有普通的方法调用时可以多态的。例如:
//: polymorphism/FieldAccess.java
package polymorphism; /* Added by Eclipse.py */
// Direct field access is determined at compile time.
class Super {
public int field = 0;
public int getField() { return field; }
}
class Sub extends Super {
public int field = 1;
public int getField() { return field; }
public int getSuperField() { return super.field; }
}
public class FieldAccess {
public static void main(String[] args) {
Super sup = new Sub(); // Upcast
System.out.println("sup.field = " + sup.field +
", sup.getField() = " + sup.getField());
Sub sub = new Sub();
System.out.println("sub.field = " +
sub.field + ", sub.getField() = " +
sub.getField() +
", sub.getSuperField() = " +
sub.getSuperField());
}
} /* Output:
sup.field = 0, sup.getField() = 1
sub.field = 1, sub.getField() = 1, sub.getSuperField() = 0
*///:~
同时,我们也应当注意一下编程的习惯:首先将所有的域设置为
private,因而不能够直接访问它们,副作用是只能用对方类中的方法去访问这些域。此外,起名字时不要混淆。
2. 构造器与多态
2.1 使用导出类时的构造顺序
通常,构造器不同于其地种类的方法。涉及到多态时仍然是如此。尽管构造器并不具有多态性(它们实际上是static方法,只不过该static声明是隐式的),但还是非常有必要理解构造器怎样通过多态在复杂的层次结构中运作。
基类的构造器总是在导出类的构造过程中被调用,面且按继承层次逐渐向上链接,以使每个基类的构造器都能得到调用。这样做是有意义的,器为构造器具有一特殊任务:检查对象是否被正确地构造。导出类只能访问它自己的成员,不能访问基类中的成员(基类成员通常是private类型)。只有基类的构造器才具有恰当的知识和权限来对自己的元素进行初始化。因此,必须使得所有的构造器都得到调用,否则就不可能正确构造完整对象。这正是编译器为什么要强制每个导出类部分都必须调用构造器的原因。在导出类的构造器主体中,如果没有明确指定某种基类构造器,它就会自动调用默认构造器。若不存在默认构造器,编译器就会报错(若某个类没有构造器,编译器会自动合成出一个默认构造器)。
复杂对象调用构造器一定要遵照下面的顺序:
- 调用默认构造器。这个步骤会不断地反复递归下去。
- 按声明顺序调用成员的初始化方法。
- 调用导出类构造器的主体。
若遵循这一规则,那么就能保证所有基类成员说及当前对象的成员对象都被初始化了。但这种捷径并不适用于所有情况。
2.2 清理
通过组合和继承的方法来创建新的类的时候,通常不需要考虑对象的清理的问题。
2.3 构造器内部的多态方法
考虑一下一段的代码:
//: polymorphism/PolyConstructors.java
package polymorphism; /* Added by Eclipse.py */
// Constructors and polymorphism
// don't produce what you might expect.
import static net.mindview.util.Print.*;
class Glyph {
void draw() { print("Glyph.draw()"); }
Glyph() {
print("Glyph() before draw()");
draw();
print("Glyph() after draw()");
}
}
class RoundGlyph extends Glyph {
private int radius = 1;
RoundGlyph(int r) {
radius = r;
print("RoundGlyph.RoundGlyph(), radius = " + radius);
}
void draw() {
print("RoundGlyph.draw(), radius = " + radius);
}
}
public class PolyConstructors {
public static void main(String[] args) {
new RoundGlyph(5);
}
} /* Output:
Glyph() before draw()
RoundGlyph.draw(), radius = 0
Glyph() after draw()
RoundGlyph.RoundGlyph(), radius = 5
*///:~
通过这个程序,我们可以更加精细的了解初始化的顺序,初始化的实际顺序为:
- 在其他任何事物发生之前,将分配给对象的存储空间初始化成二进制的零。
- 如前所述那样调用基类构造器。此时,调用被覆盖后的draw()方法。
- 按照声明的顺序调用成员的初始化方法。
- 调用导出类的构造器变体。