1. 程式人生 > >java程式設計思想-08多型

java程式設計思想-08多型

在面向物件的程式設計語言中,多型是繼資料抽象和繼承之後的第三種基本特徵。

多型通過分離做什麼和怎麼做,從另一角度將介面和實現分離開來。多型不但能夠改善程式碼的組織結構和可讀性,還能夠建立可擴充套件的程式。

“封裝”通過合併特徵和行為來建立新的資料型別。“實現隱藏”則通過將細節“私有化”把介面和實現分離開來。多型的作用則是消除型別之間和耦合關係。繼承允許將物件視為它自己本身的型別或其基型別來處理。這種能力極為重要,因為它允許將多種型別(從同一基類匯出的)視為同一型別來處理。而同一份程式碼也就可以毫無差別地執行在這些不同型別之上了。多型方法呼叫允許一種型別表現出與其他相似型別之間的區別,只要它們都是從同一基類匯出來的。這種區別是根據方法行為的不同而表示出來的,雖然這些方法都可以通過同一個基類來呼叫。

1、再論向上轉型

我們把對某個物件的引用視為對其基型別的引用的做法稱為向上轉型–因為在繼承樹的畫法中,基類是放置在上方的。為什麼我們要向上轉型成基類引用呢?主要還是為了複用程式碼。比如我們有一個car類。car有一個方法是move。不向上轉型的話,我們就需要在類開始的地方,寫呼叫各種汽車move的方法。如果可以向上轉型,就可以動態繫結方法,我們便無須再寫呼叫各種汽車的方法,只需寫呼叫car的move方法。

class H extends Car{
  public void move(){System.out.print("H Move")};
}
class K extends
Car{
public void move(){System.out.print("K Move")}; } public class Dirve{   //如果使用向上轉型,這個go方法就可以不用寫了,不然的話,以後萬一又增加一個新的Car類,比如L,那又得在這裡增加一個go(L l)方法。很不方便。   public static void go(H h){     h.move();   }   public static void go(K k){     k.move();   }   public static void main(String[] args){     H h = new
H();     K k = new K();     go(h);     go(k);   } }

2、轉機

1)方法呼叫繫結。將一個方法呼叫同一個方法主體關聯起來被稱作繫結。若在程式執行前進行繫結(如果有的話,由編譯器和連線程式實現),叫做前期繫結。但如果傳入的引數一開始編譯的時候只有基類的引用,編譯器並不知道該去呼叫哪個方法。所以,解決的辦法就是後期繫結。後期繫結的含義就是在執行時根據物件的型別進行繫結。後期繫結也叫做動態繫結或執行時繫結。如果一種語言想實現後期繫結,就必須具有某種機制,以便在執行時能判斷物件的型別,從而呼叫恰當的方法。也就是說,編譯器一直不知道物件的型別,但方法呼叫機制能找到正確的方法體,並加以呼叫。後期繫結機制隨程式語言的不同而有所不同,但是隻要想一下就會得知,不管怎樣都必須在物件中安置某種“型別資訊”。前面的例子中,只需要將go方法改成go(Car car)便可。

Java中除了static方法和final方法(private方法屬於final方法)之外,其他所有的方法都是後期繫結。將方法宣告為final後,可以防止其他人覆蓋該方法。也可以有效地“關閉”動態繫結。

2)產生正確的行為。一旦知道Java中所有方法都是通過動態繫結實現多型之後,我們就可以編寫只與基類打交道的程式碼了,並且這些程式碼對所有的匯出類都可以正確執行。或者換一種說法,傳送訊息給某個物件,讓該物件去斷定應該做什麼事。

3)可擴充套件性。由於有多型機制。我們就可以根據自己的需求對系統添任意多的新型別。而不需增加或更改go()方法。大多數或者所有方法都會遵守go()的模型,而且只與基類介面通訊。這樣的程式是可擴充套件的,因為可以從通用的基類繼承出新的資料型別,從而新添一些功能。那些操縱基類介面的方法不需要任何改動就可以應用於新類。

4)缺陷:“覆蓋”私有方法。

public class PrivateOverride{
     private void f(){ System.out.print("private f()");}
     public static void main(String[] args){
         PrivateOverride po = new Derived();
      po.f(); 
    }
}

class Derived extends PrivateOverride{
    public void f() { System.out.print("public f()"); }
}
/output:
private f()

我們所期望的輸出是public f(),但是由於private方法是自動認為是final方法,而且對匯出類是遮蔽的。因此,這種情況下。Derived類中的f()方法就是一個全新的方法,基類中的f()方法在子類Derived中不可見,因此也不能被過載。只有非private方法才可以被覆蓋。在匯出類中,對於基類中的private方法,最好採用不同的名字。

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 FieldAccess{
  public static void main(String[] args){
    Super sup = new Sub();
    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

當Sub物件轉型為Super引用時,任何域訪問操作都將由編譯器解析,因此不是多型的。但這種情況一般都不會發生,因為所有的域一般都是private的,都不能直接訪問。只有通過方法才能夠訪問(例如get方法)。

而且當一個方法是靜態的,它的行為也就不具有多型性。靜態方法是與類,而並非與單個物件相關聯的。

3、構造器和多型

首先,構造器並不具有多型性。(它們實際上是static方法,只不過該static宣告是隱式的)。

1)、構造器的呼叫順序。1.呼叫基類構造器。這個步驟會不斷地反覆遞迴下去,首先是構造這種層次結構的根,然而是下一層匯出類,等等,直到最低層的匯出類。2.按宣告順序呼叫成員的初始化方法。3.呼叫匯出類構造器的主體。

2)、繼承與清理。通過組合和繼承來建立新類時,永遠不必擔心物件的清理問題。子物件通常都會留給垃圾回收器進行處理。如果確實遇到問題,那麼必須用心為新類建立dispose()方法(名稱可以自己定)。並且由於是繼承的緣故,如果我們有其他作為垃圾回收一部分的特殊清理動作,就必須在匯出類中覆蓋dispose()方法。當覆蓋被繼承類的dispose()方法時,務必記住呼叫基類的dispose()方法。否則,基類的清理動作就不會發生。

3)、構造器內部的多型方法的行為。在一般的方法內部,動態繫結的呼叫是在執行時才決定的,因為物件無法知道它是屬於方法所在的那個類,還是屬於那個類的匯出類。如果要呼叫構造器內部的一個動態繫結方法,就要用到那個方法的被覆蓋後的定義。然而這個呼叫的效果可能相當難於預料。因為被覆蓋的方法在物件被完全構造之前就會被呼叫。這可能會造成一些難於發現的隱藏錯誤。從概念上講,構造器的工作實際上是建立物件。在任何構造器的內部,整個物件都可能只是部分形成–我們只知道基類物件已經進行初始化。如果構造器只是在構建物件過程中的一個步驟,並且該物件所屬的類是從這個構造器所屬的類匯出的。那麼匯出部分在當前構造器正在被呼叫的時刻仍舊是沒有被初始化的。然而,一個動態繫結的方法呼叫卻會向外深入到繼承層次結構內部,它可以呼叫匯出類裡的方法。如果我們是在構造器內部這樣做,那麼就有可能呼叫某個方法,而這個方法所操縱的成員可能還未進行初始化。

class Glyph{
  void draw() { System.out.print("Glyph.draw()");}
  Glyph(){
    System.out.print("Glyph() before draw()");
    draw();
    System.out.print("Glyph() after draw()");  
  }

}

class RoundGlyph extends Glyph(){
  private int radius = 1;
  RoundGlyph(int r){
    radius = r;
    System.out.print("RoundGlyph.RoundGlyph(),radius = "+radius);
  }
  void draw(){
    System.out.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

我們期望的輸出應該是1,但是一開始的輸出確是0 。這裡我們得更加詳細地介紹一下初始化順序了。

1.在其他任何事物發生之前,將分配給物件的儲存空間初始化為二進位制的零。
2.呼叫基類構造器。這個步驟會不斷地反覆遞迴下去,首先是構造這種層次結構的根,然而是下一層匯出類,等等,直到最低層的匯出類
3.按照宣告的順序呼叫成員的初始化方法。
4.呼叫匯出類的構造器主體。

4、協變返回型別

Java SE5中添加了協變返回型別,它表示在匯出類的被覆蓋方法可以返回基類方法的返回型別的某種匯出型別。

5、用繼承進行設計

當在使用現成的類來建立新類時,首先應當還是考慮“組合”。組合不會強制我們的程式設計進入繼承的層次結構中,而且,組合更加靈活,因為它可以動態選擇型別。相反,繼承在編譯時就需要知道確切型別。比如我new 一個Car 。我可以在程式執行過程中讓它從賓士變為寶馬,也可以變成奧迪(這個也稱為狀態模式)。但是繼承,我一旦new了,賓士就是賓士,並不能再變成寶馬。我們不能在執行期間決定繼承不同的物件。

1)、純繼承與擴充套件。純繼承:只有在基類中已經建立的方法才可以在匯出類中覆蓋。就是“is-a”的關係。因為一個類的介面已經確定了它應該是什麼。繼承可以確保所有的匯出類具有基類的介面,且絕對不會少。這就是一種純替代。因為匯出類可以完全替代基類,使用時,完全不需要知道關於子類的任何額外資訊。擴充套件:就是“is-like-a”的關係。因為匯出類就像是一個基類–具有相同的基本介面,但是它還具有由額外方法實現的其他特性。雖然這是一種有用的方法,但還是要依賴於具體的情況。它的缺點是,匯出類中介面的擴充套件部分不能被基類訪問。因此一旦向上轉型,就不能呼叫那些新方法。

2)、向下轉型與執行時型別識別。在Java中,所有轉型都會得到檢查。這些都是在執行期間對型別進行檢查的行為。一般來說,如果你知道這個物件的具體型別,就可以嘗試向下轉型。如果所轉型別是正確的,那麼轉型成功。如果是錯誤的,便會返回一個ClassCastException異常。