1. 程式人生 > >Java編程思想 學習筆記8

Java編程思想 學習筆記8

java語言 種類型 隱藏 基本 java se ash heat 找到 一份

八、多態

在面向對象的程序設計語言中,多態是繼數據抽象和繼承之後的第三種基本特征。

多態通過分離做什麽和怎麽做,從另一角度將接口和實現分離開來。

“封裝”通過合並特征和行為來創建新的數據類型。“實現隱藏”則通過將細節“私有化”把接口和實現分離開來。而多態的作用則是消除類型之間的耦合關系。

繼承允許將對象視為它自己本身的類型或其基類類型來加以處理。這種能力極其重要,因為它允許將多種類型視為同一類型來處理,而同一份代碼也就可以毫無差別地運行在這些不同類型之上了。多態方法調用允許一種類型表現出與其他相似類型之間的區別,只要它們都是從同一基類導出而來的。這種區別是根據方法行為的不同而表現出來的,雖然這些方法都可以通過同一個基類來調用。

1.綁定

  ①方法調用綁定   將一個方法調用同一個方法主體關聯起來被稱作綁定。若在程序執行之前進行綁定(由編譯器和連接程序實現),叫做前期綁定

  在運行時根據對象的類型進行綁定,叫做後期綁定(多態綁定或運行時綁定)。如果一種語言想實現後期綁定,就必須具有某種機制,以便在運行時能判斷對象的類型,從而調用適當的方法。也就是說,編譯器一直不知道對象的類型,但是方法調用機制能夠找到正確的方法體,並加以調用。後期綁定機制不管怎樣都必須在對象中安置某種“類型信息”。

  Java中除了static方法和final方法(private方法屬於final方法)之外,其他所有的方法都是後期綁定。

  ②產生正確的行為

  Java中所有方法都是通過動態綁定實現多態的。

  基類為自它那裏繼承而來的所有導出類建立了一個公用接口——也就是說,所有導出類都可以做出基類所有的行為。導出來通過覆蓋這些行為的定義,來為每種特殊的對象提供單獨的行為。

  ③可擴展性

  只與基類通信,這樣的程序是可擴展的,因為可以從通用的類型繼承出新的數據類型,從而增添一些功能。那些操縱基類接口的方法不需要任何改動就可以應用於新類。

  多態是一項讓程序員“將改變的事物與未變的事物分離開來”的重要技術。

  ④缺陷:“覆蓋”私有方法

  若我們試圖這樣做:

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()"); }
}

/*Output
private f()
*/

  我們所期望的輸出是public f(),但是由於private方法被自動認為是final方法(因此是前期綁定,根據引用類型判斷),而且對導出類是屏蔽的。因此,在這種情形下,Derived類中的f()方法就是一個全新的方法;既然基類中的f()方法在子類中不可見,因此甚至不能被重載。

  結論:只有非private方法才可以被覆蓋。在導出類中,對於基類中的private方法,最好采用不同的名字。

  ⑤缺陷:域與靜態方法

  只有普通的方法調用可以是多態的。例如,如果你直接訪問某個域,這個訪問就將在編譯器進行解析。任何域訪問操作都將由編譯器解析,因此不是多態的。

  如果某個方法是靜態的,它的行為就不具有多態性。靜態方法是與類,而並非與單個的對象相關聯的。

2.構造器和多態

  盡管構造器並不具有多態性(它們實際上是static方法),但還是非常有必要理解構造器怎樣通過多態在復雜的層次結構中運作。

  ※①構造器的調用順序

  基類的構造器總是在導出類的構造過程中被調用,而且按照繼承層次逐漸向上鏈接,以使每個基類的構造器都能得到調用。這樣做是有意義的,因為構造器具有一項特殊任務:檢查對象是否被正確地構造。導出類只能訪問它自己的成員,不能訪問基類中的成員(基類成員通常是private類型)。只有基類的構造器具有恰當的知識和權限來對自己的元素進行初始化。因此,必須令所有構造器都得到調用,否則就不可能正確構造完整對象。這正是為什麽編譯器要強制每個導出類都必須調用構造器的原因。在導出類的構造器主體中,如果沒有明確指定調用某個基類構造器,它都會“默默”地調用默認構造器。如果不存在默認構造器,編譯器就會報錯(若某個類沒有構造器,編譯器會自動合成出一個默認構造器)。

  看下面這個例子,它展示組合、繼承以及多態的構建順序:

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()"); }
}
class ProtableLunch extends Lunch {
    ProtableLunch() { System.out.println("ProtableLunch()"); }
}

public class Sandwich extends ProtableLunch {
    private Bread b = new Bread();
    private Cheese c = new Cheese();
    private Lettuce l = new Lettuce();
    public Sandwich() { System.out.println("Sandwich()"); }
    public static void main(String[] args) {
        new Sandwich();
    }
}

/*Output
Meal()
Lunch()
PortableLunch()
Bread()
Cheese()
Lettuce()
Sandwich()
*/

  上面的輸出結果說明調用構造器要遵循下面的順序:

  1)調用基類構造器。這個步驟會不斷地反復遞歸下去,首先是構造這種層次結構的根,然後是下一層導出類,等等,直到最低層的導出類。

  2)按聲明順序調用成員的初始化方法。

  3)調用導出類構造器的主體。

  在構造器內部,我們必須確保所要使用的成員都已經構建完成。

  ②繼承與清理

  如果我們有其他作為垃圾回收一部分的特殊清理動作,就必須在導出類中覆蓋dispose()方法。當覆蓋被繼承類的dispose()方法時,務必記住調用基類版本dispose()方法;否則基類的清理動作就不會發生。

  銷毀的順序應該與初始化順序相反。

  ③構造器內部的多態方法的行為

  構造器調用的層次結構帶來了一個有趣的兩難問題——如果在一個構造器的內部調用正在構造的對象的某個動態綁定方法,那會發生什麽情況呢?

  在一般的方法內部,動態綁定的調用時在運行時才決定的,因為對象無法知道它是屬於方法所在的那個類,還是屬於那個類的導出類。

  如果要調用構造器內部的一個動態綁定方法,就要用到那個方法的被覆蓋後的定義。然而,這個調用的效果可能難以預料,因為被覆蓋的方法在對象被完全構造前就會被調用,這可能會造成一些難以發現的隱藏錯誤。

  上面介紹的初始化順序並不完整,而這正是解決這個問題的關鍵。初始化的實際過程是:

  1) 在其他任何事物發生之前,將分配給對象的存儲空間初始化成二進制的零。

  2) 如前述那樣調用基類構造器。

  3) 按照聲明的順序調用成員的初始化方法。

  4) 調用導出類的構造器主體。

  這樣的優點是所有東西都至少初始化成“零”。

  編寫構造器時有一條有效的準則:“用盡可能簡單的方法使對象進入正常狀態;如果可以的話,避免調用其他方法。”在構造器中唯一能夠安全調用的那些方法是基類中的final方法(也適用於private方法)。

3.協變返回類型

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

class Grain {
    public String toString() { return "Grain"; }
}
class Wheat extends Grain {
    public String toString() { return " Wheat" }
}

class Mill {
    Grain process() { return  new Wheat(); }
}
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();
        g = m.process();
        System.out.println(g);
     }
}
/*Output
Grain
Wheat
*/

4.用繼承進行設計

  更好的方式是首先選擇“組合”。組合更加靈活,因為它可以動態選擇類型(因此就選擇了行為);相反,繼承在編譯時就需要知道確切類型。

class Actor {
    public void act() {}
}
class HappyActor extends Actor {
    public void act() { System.out.println("HappyActor"); }
}
class SadActor extends Actor {
    public void act() { System.out.println("SadActor"); }
}

class Stage {
    private Actor actor = new HappyActor();
    public void change() { actor = new SadActor(); }
    public void performPlay() { actor.act(); }
}

public class Transmogrify {
    public static void main(String[] args) {
        Stage stage = new Stage();
        stage.performPlay();
        stage.change();
        stage.performPlay();
    }
}
/*Output
HappyActor
SadActor
*/

  Stage對象含有一個對Actor的引用,並可以在運行時改變實際對象,然後performPlay()產生的行為也隨之改變。這樣一來,我們在運行期間獲得了動態靈活性(這也稱為狀態模式)。

  ①純繼承與擴展

  純繼承是“is-a”關系。導出類具有和基類一樣的接口,且基類可以接收發送給導出類的任何信息。

  擴展是“is-like-a”關系,因為導出類就像是一個基類——它有著相同的基本接口,但是它還具有由額外方法實現的其他特性。導出類中接口的擴展部分不能被基類訪問,因此,一旦我們向上轉型,就不能調用那些新方法。

  ②向下轉型與運行時類型識別

  由於向上轉型會丟失具體的類型信息,所以我們就想,通過向下轉型應該能夠獲取類型信息,在Java語言中,所有轉型都會得到檢查。如果類型不符,就會返回一個ClassCastException。這種在運行期間對類型進行檢查的行為稱作“運行時類型識別”(RTTI)。

5.總結   多態意味著“不同的形式”。在面向對象的程序設計中,我們持有從基類繼承而來的相同接口,以及使用該接口的不同形式:不同版本的動態綁定方法。

Java編程思想 學習筆記8