Chapter8 多态(polymorphism)
前言
多态也是OOP语言中除了数据抽象和继承以外的一个非常重要的特性。多态主要是从类的角度出发,允许继承自同一个类(父类相同)的多个类被当成是父类进行处理,也就是说我们只需要用一段代码就能够处理所有这些子类。这使得我们的代码复用性、动态性和可读性大大提高。
1. 向上造型回顾
上一章类的复用我们已经说过什么是向上造型,这里再回顾一下:简单来说,子类的引用被当成是父类的引用的现象就叫作向上造型。
示例如下:
enum Note{
MIDDLE_C,C_SHARP,B_FLAG;
}
class Instrument {
public void play(Note n) {
System.out.println("Instrument.play()");
}
}
class Wind extends Instrument{
public void play(Note n) {
System.out.println("Wind.play()");
}
}
public class Music{
public static void tune(Instrument i) {
i.play(Note.MIDDLE_C);
}
public static void main(String[] args) {
Wind wind = new Wind();
tune(wind); //upcasting
}
}
Output:
Wind.play()
但向上造型会使得“对外”的接口“变窄”,难道你对于向上造型没有疑惑吗?为什么我们在向上造型时要忘记原来的对象类型呢?为什么我们不能直接在 tune 方法的参数直接写成 Wind 或者其他继承自 Instrument 的类呢?显然,如果我们把 tune 中的参数改成每一个子类的类别,那可扩展性就大大下降了,每一次有新的子类加进来时我们都需要增加新的 tune 方法。实际上,java也会在新建的每一个对象中保存有对象类型的信息,所以我们并没有真正地忘记原来的类型。而这就是多态。
2. 多态的引出
我们再观察一下tune方法:
public static void tune(Instrument i) {
i.play(Note.MIDDLE_C);}
tune 方法接受一个 Instrument 引用,但调用的却是 Wind 中的方法。所以编译器是怎么知道这个引用指向的是一个 Wind 对象而不是一个 Instrument 对象呢?实际上编译器是不知道的,为了更加深入地理解这个问题,我们先要来说一下什么是 绑定(binding)。
2.1 方法调用绑定
将一个方法调用同一个方法主体关联起来被叫作绑定。绑定分为两种:前期绑定和后期绑定。前期绑定指的是在程序执行前进行绑定,(如果存在的话那么由编译器和连接程序实现)。这个词我们可能比较陌生,因为在面向过程语言中,比如C语言中只有这种方式,也就是前期绑定。
但上面的 tune 中的方法调用如果是前期绑定的话,那它怎么知道需要绑定哪个方法体呢?
解决办法就是后期绑定。后期绑定是指在运行时根据对象的类型进行绑定,也叫动态绑定或运行时绑定。如果想要实现后期绑定,显然我们需要一种机制,可以在运行时得到对象的类型信息,编译器机制明显不是运行时的。实际上,方法调用机制能够找到正确的方法体。再仔细一想,把这样的对象的类型信息存在对象中又显得理所当然了。不同语言后期绑定的机制也许不同,但思想都是相同的。
Java中除了static和final方法(private方法属于final方法)之外,其它所有方法都是后期绑定。
2.2 缺陷1:“覆盖”private方法
如下所示,我们期望调用的是子类当中的 f() 方法,但输出显示我们调用的却是父类中的 f() 方法。这是为什么呢?原因就在于 private 方法被自动认为是 final 方法,而且对导出类是屏蔽的。换句话说, private 方法不存在“覆盖”现象。
示例1如下:
public class Base{
private void f() {
System.out.println("Base.f()");
}
public static void main(String[] args) {
Base base = new Derived(); //upcasting
base.f();
}
}
class Derived extends Base{
public void f() {
System.out.println("Derived.f()");
}
}
Output:
Base.f()
示例2如下:
public class Derived extends Base{
public final void f() {
System.out.println("Derived.f()");
}
public static void main(String[] args) {
Base base = new Derived();
//! base.f(); //invisible
}
}
class Base{
private final void f() {
System.out.println("Base.f()");
}
}
示例3如下:
public class Base{
public void f() {
System.out.println("Base.f()");
}
public static void main(String[] args) {
Base base = new Derived();
base.f();
}
}
class Derived extends Base{
public void f() {
System.out.println("Derived.f()");
}
}
Output:
Derived.f()
2.3 缺陷2: field和static方法
虽然我们现在了解了多态机制,但并不能认为所有事情都可以多态地发生。实际上,只有普通的方法调用是多态的。而域和静态方法并不是多态的。
示例如下:
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(); //upcasting
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来修饰域,使得不能直接访问它们,当然还可以通过方法调用来访问。
另一个问题就是静态方法了。因为静态方法是与类有关,而不与具体对象有关,因此也不存在多态。
示例如下:
class Base{
public static void staticMethod() {
System.out.println("Base.staticMethod()");
}
public void dynamicMethod() {
System.out.println("Base.dynamicMethod()");
}
}
class Derived extends Base{
public static void staticMethod() {
System.out.println("Derived.staticMethod()");
}
public void dynamicMethod() {
System.out.println("Derived.dynamicMethod()");
}
}
public class StaticMethodTest{
public static void main(String[] args) {
Base base = new Derived();//upcasting
base.staticMethod();
base.dynamicMethod();
}
}
Output:
Base.staticMethod()
Derived.dynamicMethod()
3. 构造器和多态
虽然我们之前说过,构造器是特殊的方法,但是构造器并不存在多态现象。实际上,构造器是隐式的static方法。
3.1 构造器的调用顺序
基类的构造器总是在导出类的构造过程中被调用,而且是按照继承层次逐渐向上链接的,以使得基类的构造器最先得到调用。因为构造器有一个特殊的任务:检查对象是否被正确地构造。导出类只能访问自己类中的成员,只有基类的构造器才有权限访问基类的成员。因此必须令所有构造器都得到调用,否则就不可能正确构造完整对象。
示例如下:
class Meal{
Meal(){
System.out.println("Meal()");
}
}
class Bread{
Bread(){
System.out.println("Bread()");
}
}
class Cheese{
Cheese(){
System.out.println("Cheese()");
}
}
class Lettuce{
Lettuce(){
System.out.println("Lettuce()");
}
}
class Lunch extends Meal{
Lunch(){
System.out.println("Lunch()");
}
}
public class Sanwitch extends Lunch{
Sanwitch(){
System.out.println("Sanwitch()");
}
Bread bread = new Bread();
Cheese cheese = new Cheese();
Lettuce lettuce = new Lettuce();
public static void main(String[] args) {
Sanwitch sanwitch = new Sanwitch();
}
}
Output:
Meal()
Lunch()
Bread()
Cheese()
Lettuce()
Sanwitch()
3.2 构造器内部的多态方法的行为
如果在一个构造器的内部调用一个正在构在的对象的某个动态绑定方法,会发生什么呢?实际上,这有可能导致非常严重的错误,因为如果要在构造器内部调用一个动态绑定方法,就要用到那个方法的被覆盖后的定义。
示例如下:
class Glyph{
Glyph(){
System.out.println("before Glyph.draw()");
draw();
System.out.println("after Glyph.draw()");
}
void draw() {
System.out.println("Glyph.draw()");
}
}
class RoundGlyph extends Glyph{
private int radius = 1;
RoundGlyph(int r){
radius = r;
System.out.println("RoundGlyph Constructor finished(), radius = "+radius);
}
void draw() {
System.out.println("RoundGlyph.draw(), radius = "+radius);
}
}
public class PolyConstructor{
public static void main(String[] args) {
new RoundGlyph(3);
}
}
Output:
before Glyph.draw()
RoundGlyph.draw(), radius = 0
after Glyph.draw()
RoundGlyph Constructor finished(), radius = 3
如上所示,我们设计的是导出类中的 draw 方法覆盖基类中的 draw 方法,但最后的结果显然不是我们期望的。因此在平时的设计过程中,我们应尽量避免调用需要动态绑定的方法。
4. 协变返回类型
协变返回类型是指导出类中的覆盖方法可以返回基类中该被覆盖方法的返回类型的某种导出类型。
示例如下:
class Grain{
public String toString() {
return "Grain";
}
}
class Wheat extends Grain{
public String toString() {
return "Wheat";
}
}
class Mill{
Grain process() {
return new Grain();
}
}
class WheatMill extends Mill{
Wheat process() {
return new Wheat();
}
}
public class CovariantReturn{
public static void main(String[] args) {
Mill m = new Mill();
Grain g = m.process();
System.out.println(g);
m = new WheatMill();
//! Wheat w = m.process(); constructors have no polymorphism!!!
g = m.process();
System.out.println(g);
}
}
Output:
Grain
Wheat
5. 向下造型
由于向上造型会丢失具体的类型信息,那么我们也会同时想到向下造型能不能恢复丢失的信息呢?答案是肯定的,但是向下造型会设计到安全性问题,当然java已经为我们想好了,所有转型都会得到检查!如果不符合,那么会返回ClassCastException。这种在运行期间进行类型检查的行为称作“运行时类型识别”(RTTI,run-time type identification)。
示例如下:
class Useful{
public void f() {}
public void g() {}
}
class MoreUseful extends Useful{
public void f() {}
public void g() {}
public void h() {}
public void j() {}
}
public class RTTI{
public static void main(String[] args) {
Useful[] x = {
new Useful(),
new MoreUseful()
};
x[0].f();
x[1].g();
//! x[1].h();
}
}