本章概要
- 向上轉型回顧
- 忘掉對象類型
- 轉機
- 方法調用綁定
- 產生正確的行為
- 可擴展性
- 陷阱:“重寫”私有方法
- 陷阱:屬性與靜態方法
多態是面向對象編程語言中,繼數據抽象和繼承之外的第三個重要特性。
多態提供了另一個維度的接口與實現分離,以解耦做什么和怎么做。多態不僅能改善代碼的組織,提高代碼的可讀性,而且能創建有擴展性的程序——無論在最初創建項目時還是在添加新特性時都可以“生長”的程序。
封裝通過合并特征和行為來創建新的數據類型。隱藏實現通過將細節私有化把接口與實現分離。這種類型的組織機制對于有面向過程編程背景的人來說,更容易理解。而多態是消除類型之間的耦合。在上一章中,繼承允許把一個對象視為它本身的類型或它的基類類型。這樣就能把很多派生自一個基類的類型當作同一類型處理,因而一段代碼就可以無差別地運行在所有不同的類型上了。多態方法調用允許一種類型表現出與相似類型的區別,只要這些類型派生自一個基類。這種區別是當你通過基類調用時,由方法的不同行為表現出來的。
在本章中,通過一些基本、簡單的例子(這些例子中只保留程序中與多態有關的行為),你將逐步學習多態(也稱為_動態綁定_或_后期綁定_或_運行時綁定_)。
向上轉型回顧
在上一章中,你看到了如何把一個對象視作它的自身類型或它的基類類型。這種把一個對象引用當作它的基類引用的做法稱為向上轉型,因為繼承圖中基類一般都位于最上方。
同樣你也在下面的音樂樂器例子中發現了問題。即然幾個例子都要演奏樂符(Note),首先我們先在包中單獨創建一個 Note 枚舉類:
// polymorphism/music/Note.java
// Notes to play on musical instruments
package polymorphism.music;public enum Note {MIDDLE_C, C_SHARP, B_FLAT; // Etc.
}
枚舉已經在”第 6 章初始化和清理“一章中介紹過了。
這里,Wind 是一種 Instrument;因此,Wind 繼承 Instrument:
// polymorphism/music/Instrument.java
package polymorphism.music;class Instrument {public void play(Note n) {System.out.println("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:@Overridepublic void play(Note n) {System.out.println("Wind.play() " + n);}
}
Music 的方法 tune()
接受一個 Instrument 引用,同時也接受任何派生自 Instrument 的類引用:
// polymorphism/music/Music.java
// Inheritance & upcasting
// {java polymorphism.music.Music}
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}
}
輸出:
Wind.play() MIDDLE_C
在 main()
中你看到了 tune()
方法傳入了一個 Wind 引用,而沒有做類型轉換。這樣做是允許的—— Instrument 的接口一定存在于 Wind 中,因此 Wind 繼承了 Instrument。從 Wind 向上轉型為 Instrument 可能“縮小”接口,但不會比 Instrument 的全部接口更少。
忘掉對象類型
Music.java 看起來似乎有點奇怪。為什么所有人都故意忘記掉對象類型呢?當向上轉型時,就會發生這種情況,而且看起來如果 tune()
接受的參數是一個 Wind 引用會更為直觀。這會帶來一個重要問題:如果你那么做,就要為系統內 Instrument 的每種類型都編寫一個新的 tune()
方法。假設按照這種推理,再增加 Stringed 和 Brass 這兩種 Instrument :
// polymorphism/music/Music2.java
// Overloading instead of upcasting
// {java polymorphism.music.Music2}
package polymorphism.music;class Stringed extends Instrument {@Overridepublic void play(Note n) {System.out.println("Stringed.play() " + n);}
}class Brass extends Instrument {@Overridepublic void play(Note n) {System.out.println("Brass.play() " + n);}
}public class Music2 {public static void tune(Wind i) {i.play(Note.MIDDLE_C);}public static void tune(Stringed i) {i.play(Note.MIDDLE_C);}public static void tune(Brass i) {i.play(Note.MIDDLE_C);}public static void main(String[] args) {Wind flute = new Wind();Stringed violin = new Stringed();Brass frenchHorn = new Brass();tune(flute); // No upcastingtune(violin);tune(frenchHorn);}
}
輸出:
Wind.play() MIDDLE_C
Stringed.play() MIDDLE_C
Brass.play() MIDDLE_C
這樣行得通,但是有一個主要缺點:必須為添加的每個新 Instrument 類編寫特定的方法。這意味著開始時就需要更多的編程,而且以后如果添加類似 tune()
的新方法或 Instrument 的新類型時,還有大量的工作要做。考慮到如果你忘記重載某個方法,編譯器也不會提示你,這會造成類型的整個處理過程變得難以管理。
如果只寫一個方法以基類作為參數,而不用管是哪個具體派生類,這樣會變得更好嗎?也就是說,如果忘掉派生類,編寫的代碼只與基類打交道,會不會更好呢?
這正是多態所允許的。但是大部分擁有面向過程編程背景的程序員會對多態的運作方式感到一些困惑。
轉機
運行程序后會看到 Music.java 的難點。Wind.play() 的輸出結果正是我們期望的,然而它看起來似乎不應該得出這樣的結果。觀察 tune()
方法:
public static void tune(Instrument i) {// ...i.play(Note.MIDDLE_C);
}
它接受一個 Instrument 引用。那么編譯器是如何知道這里的 Instrument 引用指向的是 Wind,而不是 Brass 或 Stringed 呢?編譯器無法得知。為了深入理解這個問題,有必要研究一下_綁定_這個主題。
方法調用綁定
將一個方法調用和一個方法主體關聯起來稱作_綁定_。若綁定發生在程序運行前(如果有的話,由編譯器和鏈接器實現),叫做_前期綁定_。你可能從來沒有聽說這個術語,因為它是面向過程語言不需選擇默認的綁定方式,例如在 C 語言中就只有_前期綁定_這一種方法調用。
上述程序讓人困惑的地方就在于前期綁定,因為編譯器只知道一個 Instrument 引用,它無法得知究竟會調用哪個方法。
解決方法就是_后期綁定_,意味著在運行時根據對象的類型進行綁定。后期綁定也稱為_動態綁定_或_運行時綁定_。當一種語言實現了后期綁定,就必須具有某種機制在運行時能判斷對象的類型,從而調用恰當的方法。也就是說,編譯器仍然不知道對象的類型,但是方法調用機制能找到正確的方法體并調用。每種語言的后期綁定機制都不同,但是可以想到,對象中一定存在某種類型信息。
Java 中除了 static 和 final 方法(private 方法也是隱式的 final)外,其他所有方法都是后期綁定。這意味著通常情況下,我們不需要判斷后期綁定是否會發生——它自動發生。
為什么將一個對象指明為 final ?正如前一章所述,它可以防止方法被重寫。但更重要的一點可能是,它有效地”關閉了“動態綁定,或者說告訴編譯器不需要對其進行動態綁定。這可以讓編譯器為 final 方法生成更高效的代碼。然而,大部分情況下這樣做不會對程序的整體性能帶來什么改變,因此最好是為了設計使用 final,而不是為了提升性能而使用。
產生正確的行為
一旦當你知道 Java 中所有方法都是通過后期綁定來實現多態時,就可以編寫只與基類打交道的代碼,而且代碼對于派生類來說都能正常地工作。或者換種說法,你向對象發送一條消息,讓對象自己做正確的事。
面向對象編程中的經典例子是形狀 Shape。這個例子很直觀,但不幸的是,它可能讓初學者困惑,認為面向對象編程只適合圖形化程序設計,實際上不是這樣。
形狀的例子中,有一個基類稱為 Shape ,多個不同的派生類型分別是:Circle,Square,Triangle 等等。這個例子之所以好用,是因為我們可以直接說“圓(Circle)是一種形狀(Shape)”,這很容易理解。繼承圖展示了它們之間的關系:
向上轉型就像下面這么簡單:
Shape s = new Circle();
這會創建一個 Circle 對象,引用被賦值給 Shape 類型的變量 s,這看似錯誤(將一種類型賦值給另一種類型),然而是沒問題的,因此從繼承上可認為圓(Circle)就是一個形狀(Shape)。因此編譯器認可了賦值語句,沒有報錯。
假設你調用了一個基類方法(在各個派生類中都被重寫):
s.draw()
你可能再次認為 Shape 的 draw()
方法被調用,因為 s 是一個 Shape 引用——編譯器怎么可能知道要做其他的事呢?然而,由于后期綁定(多態)被調用的是 Circle 的 draw()
方法,這是正確的。
下面的例子稍微有些不同。首先讓我們創建一個可復用的 Shape 類庫,基類 Shape 為它的所有子類建立了公共接口——所有的形狀都可以被繪畫和擦除:
// polymorphism/shape/Shape.java
package polymorphism.shape;public class Shape {public void draw() {}public void erase() {}
}
派生類通過重寫這些方法為每個具體的形狀提供獨一無二的方法行為:
// polymorphism/shape/Circle.java
package polymorphism.shape;public class Circle extends Shape {@Overridepublic void draw() {System.out.println("Circle.draw()");}@Overridepublic void erase() {System.out.println("Circle.erase()");}
}// polymorphism/shape/Square.java
package polymorphism.shape;public class Square extends Shape {@Overridepublic void draw() {System.out.println("Square.draw()");}@Overridepublic void erase() {System.out.println("Square.erase()");}}// polymorphism/shape/Triangle.java
package polymorphism.shape;public class Triangle extends Shape {@Overridepublic void draw() {System.out.println("Triangle.draw()");}@Overridepublic void erase() {System.out.println("Triangle.erase()");}
}
RandomShapes 是一種工廠,每當我們調用 get()
方法時,就會產生一個指向隨機創建的 Shape 對象的引用。注意,向上轉型發生在 return 語句中,每條 return 語句取得一個指向某個 Circle,Square 或 Triangle 的引用, 并將其以 Shape 類型從 get()
方法發送出去。因此無論何時調用 get()
方法,你都無法知道具體的類型是什么,因為你總是得到一個簡單的 Shape 引用:
// polymorphism/shape/RandomShapes.java
// A "factory" that randomly creates shapes
package polymorphism.shape;
import java.util.*;public class RandomShapes {private Random rand = new Random(47);public Shape get() {switch(rand.nextInt(3)) {default:case 0: return new Circle();case 1: return new Square();case 2: return new Triangle();}}public Shape[] array(int sz) {Shape[] shapes = new Shape[sz];// Fill up the array with shapes:for (int i = 0; i < shapes.length; i++) {shapes[i] = get();}return shapes;}
}
array()
方法分配并填充了 Shape 數組,這里使用了 for-in 表達式:
// polymorphism/Shapes.java
// Polymorphism in Java
import polymorphism.shape.*;public class Shapes {public static void main(String[] args) {RandomShapes gen = new RandomShapes();// Make polymorphic method calls:for (Shape shape: gen.array(9)) {shape.draw();}}
}
輸出:
Triangle.draw()
Triangle.draw()
Square.draw()
Triangle.draw()
Square.draw()
Triangle.draw()
Square.draw()
Triangle.draw()
Circle.draw()
main()
方法中包含了一個 Shape 引用組成的數組,其中每個元素通過調用 RandomShapes 類的 get()
方法生成。現在你只知道擁有一些形狀,但除此之外一無所知(編譯器也是如此)。然而當遍歷這個數組為每個元素調用 draw()
方法時,從運行程序的結果中可以看到,與類型有關的特定行為奇跡般地發生了。
隨機生成形狀是為了讓大家理解:在編譯時,編譯器不需要知道任何具體信息以進行正確的調用。所有對方法 draw()
的調用都是通過動態綁定進行的。
可擴展性
現在讓我們回頭看音樂樂器的例子。由于多態機制,你可以向系統中添加任意多的新類型,而不需要修改 tune()
方法。在一個設計良好的面向對象程序中,許多方法將會遵循 tune()
的模型,只與基類接口通信。這樣的程序是可擴展的,因為可以從通用的基類派生出新的數據類型,從而添加新的功能。那些操縱基類接口的方法不需要改動就可以應用于新類。
考慮一下樂器的例子,如果在基類中添加更多的方法,并加入一些新類,將會發生什么呢:
所有的新類都可以和原有類正常運行,不需要改動 tune()
方法。即使 tune()
方法單獨存放在某個文件中,而且向 Instrument 接口中添加了新的方法,tune()
方法也無需再編譯就能正確運行。下面是類圖的實現:
// polymorphism/music3/Music3.java
// An extensible program
// {java polymorphism.music3.Music3}
package polymorphism.music3;
import polymorphism.music.Note;class Instrument {void play(Note n) {System.out.println("Instrument.play() " + n);}String what() {return "Instrument";}void adjust() {System.out.println("Adjusting Instrument");}
}class Wind extends Instrument {@Overridevoid play(Note n) {System.out.println("Wind.play() " + n);}@OverrideString what() {return "Wind";}@Overridevoid adjust() {System.out.println("Adjusting Wind");}
}class Percussion extends Instrument {@Overridevoid play(Note n) {System.out.println("Percussion.play() " + n);}@OverrideString what() {return "Percussion";}@Overridevoid adjust() {System.out.println("Adjusting Percussion");}
}class Stringed extends Instrument {@Overridevoid play(Note n) {System.out.println("Stringed.play() " + n);} @OverrideString what() {return "Stringed";}@Overridevoid adjust() {System.out.println("Adjusting Stringed");}
}class Brass extends Wind {@Overridevoid play(Note n) {System.out.println("Brass.play() " + n);}@Overridevoid adjust() {System.out.println("Adjusting Brass");}
}class Woodwind extends Wind {@Overridevoid play(Note n) {System.out.println("Woodwind.play() " + n);}@OverrideString what() {return "Woodwind";}
}public class Music3 {// Doesn't care about type, so new types// added to the system still work right:public static void tune(Instrument i) {// ...i.play(Note.MIDDLE_C);}public static void tuneAll(Instrument[] e) {for (Instrument i: e) {tune(i);}}public static void main(String[] args) {// Upcasting during addition to the array:Instrument[] orchestra = {new Wind(),new Percussion(),new Stringed(),new Brass(),new Woodwind()};tuneAll(orchestra);}
}
輸出:
Wind.play() MIDDLE_C
Percussion.play() MIDDLE_C
Stringed.play() MIDDLE_C
Brass.play() MIDDLE_C
Woodwind.play() MIDDLE_C
新方法 what()
返回一個帶有類描述的 String 引用,adjust()
提供一些樂器調音的方法。
在 main()
方法中,當向 orchestra 數組添加元素時,元素會自動向上轉型為 Instrument。
tune()
方法可以忽略周圍所有代碼發生的變化,仍然可以正常運行。這正是我們期待多態能提供的特性。代碼中的修改不會破壞程序中其他不應受到影響的部分。換句話說,多態是一項“將改變的事物與不變的事物分離”的重要技術。
陷阱:“重寫”私有方法
你可能天真地試圖像下面這樣做:
// polymorphism/PrivateOverride.java
// Trying to override a private method
// {java polymorphism.PrivateOverride}
package polymorphism;public class PrivateOverride {private void f() {System.out.println("private f()");}public static void main(String[] args) {PrivateOverride po = new Derived();po.f();}
}class Derived extends PrivateOverride {public void f() {System.out.println("public f()");}
}
輸出:
private f()
你可能期望輸出是 public f(),然而 private 方法可以當作是 final 的,對于派生類來說是隱蔽的。因此,這里 Derived 的 f()
是一個全新的方法;因為基類版本的 f()
屏蔽了 Derived ,因此它都不算是重寫方法。
結論是只有非 private 方法才能被重寫,但是得小心重寫 private 方法的現象,編譯器不報錯,但不會按我們所預期的執行。為了清晰起見,派生類中的方法名采用與基類中 private 方法名不同的命名。
如果使用了 @Override
注解,就能檢測出問題:
// polymorphism/PrivateOverride2.java
// Detecting a mistaken override using @Override
// {WillNotCompile}
package polymorphism;public class PrivateOverride2 {private void f() {System.out.println("private f()");}public static void main(String[] args) {PrivateOverride2 po = new Derived2();po.f();}
}class Derived2 extends PrivateOverride2 {@Overridepublic void f() {System.out.println("public f()");}
}
編譯器報錯信息是:
error: method does not override or
implement a method from a supertype
陷阱:屬性與靜態方法
一旦學會了多態,就可以以多態的思維方式考慮每件事。然而,只有普通的方法調用可以是多態的。例如,如果你直接訪問一個屬性,該訪問會在編譯時解析:
// polymorphism/FieldAccess.java
// 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;@Overridepublic int getField() {return field;}public int getSuperField() {return super.field;}
}public class FieldAccess {public static void main(String[] args) {Super sup = new Sub(); // UpcastSystem.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())}
}
輸出:
sup.field = 0, sup.getField() = 1
sub.field = 1, sub.getField() = 1, sub.getSuperField() = 0
當 Sub 對象向上轉型為 Super 引用時,任何屬性訪問都被編譯器解析,因此不是多態的。在這個例子中,Super.field 和 Sub.field 被分配了不同的存儲空間,因此,Sub 實際上包含了兩個稱為 field 的屬性:它自己的和來自 Super 的。然而,在引用 Sub 的 field 時,默認的 field 屬性并不是 Super 版本的 field 屬性。為了獲取 Super 的 field 屬性,需要顯式地指明 super.field。
盡管這看起來是個令人困惑的問題,實際上基本不會發生。首先,通常會將所有的屬性都指明為 private,因此不能直接訪問它們,只能通過方法來訪問。此外,你可能也不會給基類屬性和派生類屬性起相同的名字,這樣做會令人困惑。
如果一個方法是靜態(static)的,它的行為就不具有多態性:
// polymorphism/StaticPolymorphism.java
// static methods are not polymorphic
class StaticSuper {public static String staticGet() {return "Base staticGet()";}public String dynamicGet() {return "Base dynamicGet()";}
}class StaticSub extends StaticSuper {public static String staticGet() {return "Derived staticGet()";}@Overridepublic String dynamicGet() {return "Derived dynamicGet()";}
}public class StaticPolymorphism {public static void main(String[] args) {StaticSuper sup = new StaticSub(); // UpcastSystem.out.println(StaticSuper.staticGet());System.out.println(sup.dynamicGet());}
}
輸出:
Base staticGet()
Derived dynamicGet()
靜態的方法只與類關聯,與單個的對象無關。