Thinking in Java chapter 9 多态

目录

#+KEYWORD

1 简介

继数据抽象(data abstraction)和继承( Inheritance )之后,多态(polymorphism)是 Java 语言支持的又一重要特性。

使用多态,可以针对同一个操作输入多种类型的数据。多态指的是同一操作支持多中数据类型。这在动态语言中显得尤为明显,比如 Python 。在 Java 中,我们也碰到过多态的例子,比如对于 + 如果我们 1+2 我们期待输出的结果是 3 ,此时, + 的功能是数学里的加法运算,但是对于 'a' + 'bc' 这样的运算,我们希望输出的是 'abc' 也就是字符串的级联。这个小例子体现了 + 的多种形态。

多态往往和动态绑定等价。在很多地方 polymorphism 和 dynamic binding , late binding, runtime binding 相提并论。

2 重访 upcasting

我们知道通过继承,一个对象的类型可以是当前类(class),也可以是其父类。把一个对象的类型当作其父类来处理就叫做 upcasting. 但是通过下面的这个例子,我们就会看到 upcasting 会导致一个问题。

这是一个关于音乐的例子,由于多个类都要用到 Notes,比如 C 小调之类的专有名词,我们首先建立一个 enum

1: //: polymorphism/music/Note.java
2: // Notes to play on musical instruments.
3: package polymorphism.music;
4: 
5: public enum Note {
6:     MIDDLE_C, C_SHARP, B_FLAT; // Etc.
7: } ///:~

然后, Wind 是一个乐器 InstrumentWind 类就继承自 Instrument 类。

Instrument 类:

 1: //: polymorphism/music/Instrument.java
 2: package polymorphism.music;
 3: import static net.mindview.util.Print.*;
 4: 
 5: class Instrument {
 6:   public void play(Note n) {
 7:     print("Instrument.play()");
 8:   }
 9: }
10:  ///:~

Wind 类:

 1: //: reusing/Wind.java
 2: // Inheritance & upcasting.
 3: package polymorphism.music;
 4: 
 5: 
 6: // Wind objects are instruments
 7: // because they have the same interface:
 8: public class Wind extends Instrument {
 9:   public void play(Note n){
10:    System.out.println("Wind.play" + n);
11:   }
12: } ///:~

最后是 Music 类:

 1: //: polymorphism/music/Music.java
 2: // Inheritance & upcasting.
 3: package polymorphism.music;
 4: 
 5: public class Music {
 6:   public static void tune(Instrument i) {
 7:     // ...
 8:     i.play(Note.MIDDLE_C);
 9:   }
10:   public static void main(String[] args) {
11:     Wind flute = new Wind();
12:     tune(flute); // Upcasting
13:   }
14: } /* Output:
15: Wind.play() MIDDLE_C
16: *///:~

注意, Music 类的 Music.tune 方法接受了一个 Instrument 类型的对象。这说明任何从 Instrument 类继承下来的类的对象都可以送给 Music.tune. 在 Musicmain 函数中,我们送给 tune 的就是 Wind 类型的对象。这是没有问题的,因为 Wind 类继承自 Instrument 类。在这里,通过给 tune 函数送入 Instrument 类型的对象,而不是 Wind 类型的对象,我们节约了大量的代码量。想象一下,乐器有好多种,也就是从 Instrument 类可以继承下来很多对象。如果我们为每一个乐器都写一个 tune 函数,这将是多么无聊的事情。

看如下无聊的代码:

 1: //: polymorphism/music/Music2.java
 2: // Overloading instead of upcasting.
 3: package polymorphism.music;
 4: import static net.mindview.util.Print.*;
 5: 
 6: class Stringed extends Instrument {
 7:   public void play(Note n) {
 8:     print("Stringed.play() " + n);
 9:   }
10: }
11: 
12: class Brass extends Instrument {
13:   public void play(Note n) {
14:     print("Brass.play() " + n);
15:   }
16: }
17: 
18: public class Music2 {
19:   public static void tune(Wind i) {
20:     i.play(Note.MIDDLE_C);
21:   }
22:   public static void tune(Stringed i) {
23:     i.play(Note.MIDDLE_C);
24:   }
25:   public static void tune(Brass i) {
26:     i.play(Note.MIDDLE_C);
27:   }
28:   public static void main(String[] args) {
29:     Wind flute = new Wind();
30:     Stringed violin = new Stringed();
31:     Brass frenchHorn = new Brass();
32:     tune(flute); // No upcasting
33:     tune(violin);
34:     tune(frenchHorn);
35:   }
36: } /* Output:
37: Wind.play() MIDDLE_C
38: Stringed.play() MIDDLE_C
39: Brass.play() MIDDLE_C
40: *///:~

上面的代码可以工作,但是这个代码结构有一个致命的问题:你必须为每一个乐器编写 tune 函数。 那么,如果能够只写一次 tune 方法,且送入的对象是 Instrument 类型,而不是 Instrument 的任一子类,岂不是更好?也就是说,在使用 tune 方法的时候,我们如果忘记 tune 的参数类型岂不是更好?这就是多态带来的福利。

3 纠结

现在,我们有一个问题, Java 在编译的过程中怎么知道 Instrument 类指向的是 Wind 而不是 Brass 或者 Stringed 类?答案是:编译器不知道。为理解这个问题,我们探讨 binding 的原理。

3.1 方法调用 binding

把方法调用和方法本身连接起来的过程叫做 binding 。程序执行之前的 binding 叫做 early binding (可能由编译器和链接器来完成)。 C 语言种的 binding 都是 early binding。但是,我们前面的例子告诉我们 Java 中, binding 的方式有些不一样。因为编译器不知道到底该调用哪个方法。所以 Java 中的 binding 方法是所谓的 late binding .意味着 binding 发生在程序运行时。late binding 也叫做 dynamic binding 或者 runtime binding。

Java 中除了 staticfinal 方法外,所有的方法都是 late binding .这意味着,你不需要显示的为某个方法指示用什么 binding 方式。Java 已经帮你安排好了。所有的 private 方法都是 final 类型的。指定一个方法为 final 意味着,你不想这个方法动态绑定。这样做的一个好处是编译器会编译出更高效的代码。但是,你不能因为性能的借口,到处使用 final ,你应该处于设计的原因使用 final ,毕竟使用 final 带来的性能提升没有那么多。

3.2 正确的行为

一旦知晓 Java 的动态绑定,我们就可以写出支持多态的漂亮代码。你的代码针对的类型不只是当前类,当前类的父类,父类的父类的对象都可以作为参数。这在 Python 中也有明显的体现(Python 是完全动态的语言)。 我们使用一个经常用到的例子来阐述与 动态绑定相关的概念。

20170730shape.png

Figure 1: 动态绑定

根据上图:

Shape s = new Circle();

这个语句生成了 Circle 对象,生成的结果转换成了 Shape 类型。貌似,这是一个错误,毕竟我们把一种类型的对象转换成了另一种类型。但是,这是可以的,因为 Circle 类继承自 Shape . 问题来了,

s.draw();

调用的是哪个函数?你可能会认为调用的是 Shapedraw() 函数,因为 s 被转换成了 Shape 类型。但是,这里调用的是 Circle.draw() . 为甚么?因为多态。

接下来,看代码,首先是 Shape 类:

1: //: polymorphism/shape/Shape.java
2: package polymorphism.shape;
3: 
4: public class Shape {
5:   public void draw() {}
6:   public void erase() {}
7: } ///:~

Circle 类:

1: //: polymorphism/shape/Circle.java
2: package polymorphism.shape;
3: import static net.mindview.util.Print.*;
4: 
5: public class Circle extends Shape {
6:   public void draw() { print("Circle.draw()"); }
7:   public void erase() { print("Circle.erase()"); }
8: } ///:~

Square 类:

1: //: polymorphism/shape/Square.java
2: package polymorphism.shape;
3: import static net.mindview.util.Print.*;
4: 
5: public class Square extends Shape {
6:   public void draw() { print("Square.draw()"); }
7:   public void erase() { print("Square.erase()"); }
8: } ///:~

Triangle 类:

1: //: polymorphism/shape/Triangle.java
2: package polymorphism.shape;
3: import static net.mindview.util.Print.*;
4: 
5: public class Triangle extends Shape {
6:   public void draw() { print("Triangle.draw()"); }
7:   public void erase() { print("Triangle.erase()"); }
8: } ///:~

随机生成形状类:

 1: //: polymorphism/shape/RandomShapeGenerator.java
 2: // A "factory" that randomly creates shapes.
 3: package polymorphism.shape;
 4: import java.util.*;
 5: 
 6: public class RandomShapeGenerator {
 7:   private Random rand = new Random(47);
 8:   public Shape next() {
 9:     switch(rand.nextInt(3)) {
10:       default:
11:       case 0: return new Circle();
12:       case 1: return new Square();
13:       case 2: return new Triangle();
14:     }
15:   }
16: } ///:~

调用以上代码:

 1: //: polymorphism/Shapes.java
 2: // Polymorphism in Java.
 3: import polymorphism.shape.*;
 4: 
 5: public class Shapes {
 6:   private static RandomShapeGenerator gen =
 7:     new RandomShapeGenerator();
 8:   public static void main(String[] args) {
 9:     Shape[] s = new Shape[9];
10:     // Fill up the array with shapes:
11:     for(int i = 0; i < s.length; i++)
12:       s[i] = gen.next();
13:     // Make polymorphic method calls:
14:     for(Shape shp : s)
15:       shp.draw();
16:   }
17: } /* Output:
18: Triangle.draw()
19: Triangle.draw()
20: Square.draw()
21: Triangle.draw()
22: Square.draw()
23: Triangle.draw()
24: Square.draw()
25: Triangle.draw()
26: Circle.draw()
27: *///:~

基类 Shape 构建了所有子类的函数接口:所有的子类都可以被 draweraseRandomShapeGenerator 是一个形状工厂,随机生成形状对象,并保存在 s 中。每一个类对象是 Circle Square 或者 Triangle 中的一个,但是在 return 的时候, upcast 成为 Shape 类对象。因此当调用 next() 时,返回的永远是 Shape 类对象。

main() 函数通过调用 RandomShapeGenerator.next() 生成了 9 个 Shape 类对象,保存在 s 中。在这个时候,你知道你有一个 Shape 的数组,但是你不知道,生成的到底时 Circle Square 还是 Triangle 。但是,当你逐个调用 draw() 函数的时候,奇妙的事情发生了, Java 居然可以知道每一个 Shape 该执行哪一个 draw() 这个就是多态。

3.3 可扩展性

现在我们重新考虑上面乐器的例子。如上所述,使用多态技术,我们可以在不改变 tune() 方法的前提下,添加任意多种乐器类型。在一个设计考究的面向对象系统中,许多方法都具有类似 tune() 的模式,这些子类只与基类通信,但是使用的却是各自的 tune() 实现。这种代码就由很强的可扩展性。因为通过继承基类的数据类型,我们可以添加新的功能。最重要的,我们不需要改变基类的实现,我们使用的仅仅是基类的接口,真正的实现在各个子类的方法中。

2是一个扩展的例子。

20170805extensibility.png

Figure 2: extensibility

所有这些新类都和原来的 tune() 相安无事。即便我们把 tune() 放到一个分离的文件中,并为 Instrument 类上添加新的方法, tune() 依然会工作的很好(我们甚至不用重新编译这个文件)。

2 所示的代码如下:

 1: //: polymorphism/music3/Music3.java
 2: // An extensible program.
 3: package polymorphism.music3;
 4: import polymorphism.music.Note;
 5: import static net.mindview.util.Print.*;
 6: 
 7: class Instrument {
 8:   void play(Note n) { print("Instrument.play() " + n); }
 9:   String what() { return "Instrument"; }
10:   void adjust() { print("Adjusting Instrument"); }
11: }
12: 
13: class Wind extends Instrument {
14:   void play(Note n) { print("Wind.play() " + n); }
15:   String what() { return "Wind"; }
16:   void adjust() { print("Adjusting Wind"); }
17: }
18: 
19: class Percussion extends Instrument {
20:   void play(Note n) { print("Percussion.play() " + n); }
21:   String what() { return "Percussion"; }
22:   void adjust() { print("Adjusting Percussion"); }
23: }
24: 
25: class Stringed extends Instrument {
26:   void play(Note n) { print("Stringed.play() " + n); }
27:   String what() { return "Stringed"; }
28:   void adjust() { print("Adjusting Stringed"); }
29: }
30: 
31: class Brass extends Wind {
32:   void play(Note n) { print("Brass.play() " + n); }
33:   void adjust() { print("Adjusting Brass"); }
34: }
35: 
36: class Woodwind extends Wind {
37:   void play(Note n) { print("Woodwind.play() " + n); }
38:   String what() { return "Woodwind"; }
39: }
40: 
41: public class Music3 {
42:   // Doesn't care about type, so new types
43:   // added to the system still work right:
44:   public static void tune(Instrument i) {
45:     // ...
46:     i.play(Note.MIDDLE_C);
47:   }
48:   public static void tuneAll(Instrument[] e) {
49:     for(Instrument i : e)
50:       tune(i);
51:   }
52:   public static void main(String[] args) {
53:     // Upcasting during addition to the array:
54:     Instrument[] orchestra = {
55:       new Wind(),
56:       new Percussion(),
57:       new Stringed(),
58:       new Brass(),
59:       new Woodwind()
60:     };
61:     tuneAll(orchestra);
62:   }
63: } /* Output:
64: Wind.play() MIDDLE_C
65: Percussion.play() MIDDLE_C
66: Stringed.play() MIDDLE_C
67: Brass.play() MIDDLE_C
68: Woodwind.play() MIDDLE_C
69: *///:~

在上面的代码中,新添加了 what() ,该函数返回一个描述当前类的字符对象。另一个新添加的方法是 adjust() ,该方法提供了调节每一个乐器的方法。

main() 函数中,当在 orchestra 数组中放置一组乐器时,这些乐器自动升级为 Instrument

可以看到 tune() 函数完全忽略了其他代码的变化,依然和整个程序配合的很好。这就是多态带来的福利:只改变那些应该被改变的。

3.4 private 无法被覆盖

看下面代码:

 1: //: polymorphism/PrivateOverride.java
 2: // Trying to override a private method.
 3: package polymorphism;
 4: import static net.mindview.util.Print.*;
 5: 
 6: public class PrivateOverride {
 7:   private void f() { print("private f()"); }
 8:   public static void main(String[] args) {
 9:     PrivateOverride po = new Derived();
10:     po.f();
11:   }
12: }
13: 
14: class Derived extends PrivateOverride {
15:   public void f() { print("public f()"); }
16: } /* Output:
17: private f()
18: *///:~

有可能,你会问:问什么输出不是 public f() ?这是 private 类型函数自带 final 标签,无法被覆盖。所以在 Derived 类中的函数是一个新函数。

3.5 对域的处理

一旦你学了多态,就想处处使用多态。但是,只有普通函数支持多态。比如,如果你读取一个子域,这个过程是在编译阶段完成的。看代码:

 1: //: polymorphism/FieldAccess.java
 2: // Direct field access is determined at compile time.
 3: 
 4: class Super {
 5:   public int field = 0;
 6:   public int getField() { return field; }
 7: }
 8: 
 9: class Sub extends Super {
10:   public int field = 1;
11:   public int getField() { return field; }
12:   public int getSuperField() { return super.field; }
13: }
14: 
15: public class FieldAccess {
16:   public static void main(String[] args) {
17:     Super sup = new Sub(); // Upcast
18:     System.out.println("sup.field = " + sup.field +
19:       ", sup.getField() = " + sup.getField());
20:     Sub sub = new Sub();
21:     System.out.println("sub.field = " +
22:       sub.field + ", sub.getField() = " +
23:       sub.getField() +
24:       ", sub.getSuperField() = " +
25:       sub.getSuperField());
26:   }
27: } /* Output:
28: sup.field = 0, sup.getField() = 1
29: sub.field = 1, sub.getField() = 1, sub.getSuperField() = 0
30: *///:~

在上面的例子中,我们发现对于 field 的读取是在编译阶段完成的。但是对于函数 getField 的调用却是多态的。

3.6static 方法的处理

如果一个方法是 static 的,那么这个方法不具有多态属性。

看代码:

 1: //: polymorphism/StaticPolymorphism.java
 2: // Static methods are not polymorphic.
 3: 
 4: class StaticSuper {
 5:   public static String staticGet() {
 6:     return "Base staticGet()";
 7:   }
 8:   public String dynamicGet() {
 9:     return "Base dynamicGet()";
10:   }
11: }
12: 
13: class StaticSub extends StaticSuper {
14:   public static String staticGet() {
15:     return "Derived staticGet()";
16:   }
17:   public String dynamicGet() {
18:     return "Derived dynamicGet()";
19:   }
20: }
21: 
22: public class StaticPolymorphism {
23:   public static void main(String[] args) {
24:     StaticSuper sup = new StaticSub(); // Upcast
25:     System.out.println(sup.staticGet());
26:     System.out.println(sup.dynamicGet());
27:   }
28: } /* Output:
29: Base staticGet()
30: Derived dynamicGet()
31: *///:~

我们可以看到, static 方法不具有多态属性,具有多态属性的是普通函数。

4 构造函数和多态

构造函数和其他函数不一样,当构造函数和多态碰撞,其发生的化学反应更是不同。首先,构造函数不具有多态属性,因为构造函数是不带 staticstatic 函数。理解构造函数和多态的结合至关重要,可以帮助我们减少很多不必要的麻烦。

4.1 构造函数的调用顺序

基类的构造函数总是在继承类的构造过程中被调用。如果有多级的继承关系,那么最上面的类的构造函数会被首先调用,依次类推。这样做的好处是保证类被正确的创建。由于 private 只能通过构造函数设置,所以保证构造函数的调用至关重要。

看代码:

 1: //: polymorphism/Sandwich.java
 2: // Order of constructor calls.
 3: package polymorphism;
 4: import static net.mindview.util.Print.*;
 5: 
 6: class Meal {
 7:   Meal() { print("Meal()"); }
 8: }
 9: 
10: class Bread {
11:   Bread() { print("Bread()"); }
12: }
13: 
14: class Cheese {
15:   Cheese() { print("Cheese()"); }
16: }
17: 
18: class Lettuce {
19:   Lettuce() { print("Lettuce()"); }
20: }
21: 
22: class Lunch extends Meal {
23:   Lunch() { print("Lunch()"); }
24: }
25: 
26: class PortableLunch extends Lunch {
27:   PortableLunch() { print("PortableLunch()");}
28: }
29: 
30: public class Sandwich extends PortableLunch {
31:   private Bread b = new Bread();
32:   private Cheese c = new Cheese();
33:   private Lettuce l = new Lettuce();
34:   public Sandwich() { print("Sandwich()"); }
35:   public static void main(String[] args) {
36:     new Sandwich();
37:   }
38: } /* Output:
39: Meal()
40: Lunch()
41: PortableLunch()
42: Bread()
43: Cheese()
44: Lettuce()
45: Sandwich()
46: *///:~

在这个例子中,我们看到的层级关系是 Meal->Lunch->PortableLunch->Sandwich 。所以在创建 Sandwich 对象时,最先调用 Meal 的构造函数,然后是 Lunch , PortableLunch 接下来是类主体中的 Bread Cheese Lettuce 最后是 main 函数中的 new Sandwich() .

当有继承过程时,你需要清楚所有的基类,并且可以访问所有基类的带有 publicprotected 的成员。