標籤:多態 向上轉型 方法調用綁定 動態綁定
第八章 多態
在物件導向的程式設計語言中,多態是繼抽象和技能之後的第三種基本特徵。多態不但能夠改善代碼的組織圖和可讀性,還能夠建立可擴充的程式。
多態的作用是用來消除類型之間的耦合關係。
8.1 再論向上轉型
將某個對象的引用視為對其基類對象的做法被稱作向上轉型。但是這樣做也有問題。看如下的例子:
public enum Note {
MIDDLE_C, C_SHARP, B_FlAT;
}
class Instrument { public void play(Note n) { System.out.println("Instrument.play()"); }}class Wind extends Insrument { public void play(Note n) { System.out.println("wind.play() + " n); }}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); }}output: wind.play() MIDDLE_C
Music.tune()方法接受一個Instrument引用,同時也接受任何匯出自Instrument類。當tune()方法中接受到一個wind引用時,會自動向上轉型,這樣做時允許的,因為instrument中的介面再wind中都存在。從wind向上轉型會縮小介面,但是不會小於Instrument的介面。
8.1.1 忘記物件類型
如果讓tune()方法直接接受一個wind引用作為自己的參數,表面上更加直觀,但是每加入一種新的樂器(也就是Instrument的子類),相應的tune方法就需要重載一次。但是如果利用了多態,將instrument的多個匯出類引用看作時instrument的引用,就會輕鬆很多。
8.2 轉機
在運行上面的程式之後,傳入instrument的匯出類引用給tune方法,它也能識別到底是哪個匯出類,很多人都會奇怪,編譯器怎麼知道的呢?其實編譯器根本就不知道。。。。
8.2.1 方法調用綁定
將一個方法調用同一個方法主體關聯起來被稱作綁定。綁定分為前期綁定和後期綁定。
- 前期綁定:面向過程中不需要選擇就預設綁定的方式。
- 後期綁定:運行時根據對象的類型進行綁定。也稱作動態綁定或者運行時綁定。
Java中除了static方法和final方法(private方法屬於final方法)之外,其他所有的方法都是後期綁定的。
8.2.2 產生正確的行為
Java中所有方法都是通過動態綁定實現多態,我們可以只編寫與基類打交道的程式碼了,並且這些代碼都可以對所有匯出類正確的運行。例如:
class Shape { public void draw() {} public void erase() {}}class Circle extends Shape { public void draw() { System.out.println("Circle.draw()"); } public void erase() { System.out.println("Circle.erase()"); }}class Square extends Shape { public void draw() { System.out.println("Square.draw()"); } public void erase() { System.out.println("Square.erase()"); }}class Triangle extends Shape { public void draw() { System.out.println("Triangle.draw()"); } public void erase() { System.out.println("Triangle.erase()"); }}class RandomShapeGenerator { private Random rand = new Random(47); public Shape next() { switch(rand.nextInt(3)) { default: case 0: return new Circle(); case 1: return new Square(); case 2: return new Triangle(); } }}public class Test { public static void main(String[] args) { RandomShapeGenerator rsg = new RandomShapeGenerator(); Shape[] s = new Shape[9]; for(int i = 0; i < s.length; i++) { s[i] = rsg.next(); } for(Shape shp : s) shp.draw(); }}輸出:Triangle.draw()Triangle.draw()Square.draw()Triangle.draw()Square.draw()Triangle.draw()Square.draw()Triangle.draw()Circle.draw()
上面的例子中,向上轉型發生在return中,每次返回Circle、Square、Triangle三個其中的一個引用,但是我們通過next()方法獲得的總是一個通用的Shape引用,再來利用動態綁定實現多態。
8.2.3 可擴充性
針對上面的兩種方法(draw和erase),任何Shape的匯出類,覆蓋了這兩個介面,利用動態綁定,我們都能夠很好的對其進行使用,不用管每個匯出類是如何?的。
8.2.4 缺陷:對私人方法的覆蓋
例如:
public class PrivateOverride { private void f() { System.out.println("private f()"); } public static void main(String[] args) { Private Override po = new Deived(); po.f(); }}class Derived extends PrivateOverride { public void f() { System.out.println("public f()"); } }
像上面那樣做,我們期待的結果是”public f()”, 但是結果為”private f()”,因為private屬於final類方法,其不可以被覆蓋,而且屬於前期綁定,而非動態綁定,因此無法實現動態綁定。
8.2.5 缺陷:域與靜態方法
一旦瞭解了多態機制,可能會認為所有事物都可以多態的發生,然而只有普通方法才表現出多態。如果直接存取某個域,將不表現為多態。例如:
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 JavaTest{ public static void main(String[] args) { Super sup = new Sub(); //向上轉型 //這裡在直接存取域的時候,並沒有出現理想中的多態 System.out.println("sup.field: " + sup.field + "\nsup.getField: " + sup.getField() + "\n"); Sub sub = new Sub(); System.out.println("sub.field: " + sub.field + "\nsub.getField: " + sub.getField() + "\nsub.getSuperField():" + sub.getSuperField()); }}輸出為:sup.field: 0sup.getField: 1sub.field: 1sub.getField: 1sub.getSuperField():0
當Sub對象轉型為Super引用時,任何域訪問操作都將由編譯器解析,由於多態採用的是動態綁定,而不是靠編譯器,所以無法完成多態。
在上面的例子中,一個sub對象中有兩個叫做field的域(Super.field和Sub.field),然而在引用sub中的field時所產生的預設域並非Super版本的field,因此必須顯示的指明super.field。
8.3 構造器和多態
通常構造器不同於其他的方法,涉及到多態的時候也是如此。儘管構造器並不具有多態性(實際上構造器是static方法,只不過是隱式的static聲明)。
8.3.1 構造器的調用順序
在之前的[學習筆記](“http://blog.csdn.net/jing_unique_da/article/details/45530563“)中也提到過了初始化的順序,調用的順序是:
- 調用基類的構造器。這個步驟一直遞迴下去,直至遞迴到根類,再開始從根嚮導出類開始初始化。
- 按聲明順序調用成員的初始化方法。
- 調用匯出類構造器的主體。
8.3.3 構造器內部的多態方法的行為
現在來分析在一個構造器內部調用正在構造的對象的某個動態Binder 方法時,會發生什麼事情。例如:
class Super { public int field; public Super() { System.out.println("Super() Before"); getField(); System.out.println("Super() after"); } public int getField() { System.out.println("Super() " + field); return field; }}class Sub extends Super { public int field = 1; public Sub(int i) { field = i; System.out.println("Sub() " + field); } public int getField() { System.out.println("Sub() " + field); return field; }}public class JavaTest{ public static void main(String[] args) { new Sub(5); }}輸出為:Super() BeforeSub() 0Super() afterSub() 5
這裡的getField()方法被覆蓋了,但是在Super類的構造器中調用的getField方法並不是Super的,而是覆蓋之後的方法,而且結果也不是5,而是0。原因在於前面講述的構造順序不完整。初始化的實際過程是:
- 在其他任何事情發生之前,先將分配給對象的儲存空間初始化成二進位的零。
- 遞迴調用基類的構造器。
- 按照聲明的順序調用成員的初始化方法。
- 調用匯出類的構造器主體。
8.4 協變傳回型別
Java SE5中添加了協變傳回型別,也就是在匯出類中的被覆蓋方法可以返回基類方法的傳回型別的某種匯出類型,話有些繞口,直接上例子:
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 JavaTest{ public static void main(String[] args) { Mill m = new Mill(); Grain g = m.process(); System.out.println(g); m = new WheatMill(); g = m.process(); System.out.println(g); }}結果:GrainWheat
Thinking In Java筆記(第八章 多態)