1. 程式人生 > >Java 面向物件程式設計之多型

Java 面向物件程式設計之多型

轉自:chenssy
java提高篇之理解java的三大特性——多型

封裝隱藏了類的內部實現機制,可以在不影響使用的情況下改變類的內部結構,同時也保護了資料。對外界而已它的內部細節是隱藏的,暴露給外界的只是它的訪問方法。

繼承是為了重用父類程式碼。兩個類若存在IS-A的關係就可以使用繼承。,同時繼承也為實現多型做了鋪墊。那麼什麼是多型呢?多型的實現機制又是什麼?請看我一一為你揭開:

所謂多型就是指程式中定義的引用變數所指向的具體型別和通過該引用變數發出的方法呼叫在程式設計時並不確定,而是在程式執行期間才確定,即一個引用變數倒底會指向哪個類的例項物件,該引用變數發出的方法呼叫到底是哪個類中實現的方法,必須在由程式執行期間才能決定。因為在程式執行時才確定具體的類,這樣,不用修改源程式程式碼,就可以讓引用變數繫結到各種不同的類實現上,從而導致該引用呼叫的具體方法隨之改變,即不修改程式程式碼就可以改變程式執行時所繫結的具體程式碼,讓程式可以選擇多個執行狀態,這就是多型性。

比如你是一個酒神,對酒情有獨鍾。某日回家發現桌上有幾個杯子裡面都裝了白酒,從外面看我們是不可能知道這是些什麼酒,只有喝了之後才能夠猜出來是何種酒。你一喝,這是劍南春、再喝這是五糧液、再喝這是酒鬼酒….在這裡我們可以描述成如下:

酒 a = 劍南春

酒 b = 五糧液

酒 c = 酒鬼酒

這裡所表現的的就是多型。劍南春、五糧液、酒鬼酒都是酒的子類,我們只是通過酒這一個父類就能夠引用不同的子類,這就是多型——我們只有在執行的時候才會知道引用變數所指向的具體例項物件。

誠然,要理解多型我們就必須要明白什麼是“向上轉型”。在繼承中我們簡單介紹了向上轉型,這裡就在囉嗦下:在上面的喝酒例子中,酒(Win)是父類,劍南春(JNC)、五糧液(WLY)、酒鬼酒(JGJ)是子類。我們定義如下程式碼:

JNC a = new JNC();

對於這個程式碼我們非常容易理解無非就是例項化了一個劍南春的物件嘛!但是這樣呢?

Wine a = new JNC();

在這裡我們這樣理解,這裡定義了一個Wine 型別的a,它指向JNC物件例項。由於JNC是繼承與Wine,所以JNC可以自動向上轉型為Wine,所以a是可以指向JNC例項物件的。這樣做存在一個非常大的好處,在繼承中我們知道子類是父類的擴充套件,它可以提供比父類更加強大的功能,如果我們定義了一個指向子類的父類引用型別,那麼它除了能夠引用父類的共性外,還可以使用子類強大的功能。

但是向上轉型存在一些缺憾,那就是它必定會導致一些方法和屬性的丟失,而導致我們不能夠獲取它們。所以父類型別的引用可以呼叫父類中定義的所有屬性和方法,對於只存在與子類中的方法和屬性它就望塵莫及了。

public class Wine {
    public void fun1(){
        System.out.println("Wine 的Fun.....");
        fun2();
    }
    
    public void fun2(){
        System.out.println("Wine 的Fun2...");
    }
}

public class JNC extends Wine{
    /**
     * @desc 子類過載父類方法
     *        父類中不存在該方法,向上轉型後,父類是不能引用該方法的
     * @param a
     * @return void
     */
    public void fun1(String a){
        System.out.println("JNC 的 Fun1...");
        fun2();
    }
    
    /**
     * 子類重寫父類方法
     * 指向子類的父類引用呼叫fun2時,必定是呼叫該方法
     */
    public void fun2(){
        System.out.println("JNC 的Fun2...");
    }
}

public class Test {
    public static void main(String[] args) {
        Wine a = new JNC();
        a.fun1();
    }
}
-------------------------------------------------
Output:
Wine 的Fun.....
JNC 的Fun2...

從程式的執行結果中我們發現,a.fun1()首先是執行父類Wine中的fun1().然後再執行子類JNC中的fun2()。

分析:在這個程式中子類JNC過載了父類Wine的方法fun1(),重寫fun2(),而且過載後的fun1(String a)與 fun1()不是同一個方法,由於父類中沒有該方法,向上轉型後會丟失該方法,所以執行JNC的Wine型別引用是不能引用fun1(String a)方法。而子類JNC重寫了fun2() ,那麼指向JNC的Wine引用會呼叫JNC中fun2()方法。

所以對於多型我們可以總結如下:

指向子類的父類引用由於向上轉型了,它只能訪問父類中擁有的方法和屬性,而對於子類中存在而父類中不存在的方法,該引用是不能使用的,儘管是過載該方法。若子類重寫了父類中的某些方法,在呼叫該些方法的時候,必定是使用子類中定義的這些方法(動態連線、動態呼叫)。

對於面向物件而已,多型分為編譯時多型和執行時多型。其中編輯時多型是靜態的,主要是指方法的過載,它是根據引數列表的不同來區分不同的函式,通過編輯之後會變成兩個不同的函式,在執行時談不上多型。而執行時多型是動態的,它是通過動態繫結來實現的,也就是我們所說的多型性。

多型的實現

2.1實現條件
在剛剛開始就提到了繼承在為多型的實現做了準備。子類Child繼承父類Father,我們可以編寫一個指向子類的父類型別引用,該引用既可以處理父類Father物件,也可以處理子類Child物件,當相同的訊息傳送給子類或者父類物件時,該物件就會根據自己所屬的引用而執行不同的行為,這就是多型。即多型性就是相同的訊息使得不同的類做出不同的響應。

Java實現多型有三個必要條件:繼承、重寫、向上轉型。

繼承:在多型中必須存在有繼承關係的子類和父類。

重寫:子類對父類中某些方法進行重新定義,在呼叫這些方法時就會呼叫子類的方法。

向上轉型:在多型中需要將子類的引用賦給父類物件,只有這樣該引用才能夠具備技能呼叫父類的方法和子類的方法。

只有滿足了上述三個條件,我們才能夠在同一個繼承結構中使用統一的邏輯實現程式碼處理不同的物件,從而達到執行不同的行為。

對於Java而言,它多型的實現機制遵循一個原則:當超類物件引用變數引用子類物件時,被引用物件的型別而不是引用變數的型別決定了呼叫誰的成員方法,但是這個被呼叫的方法必須是在超類中定義過的,也就是說被子類覆蓋的方法。

2.2實現形式

在Java中有兩種形式可以實現多型。繼承和介面。

2.2.1、基於繼承實現的多型

基於繼承的實現機制主要表現在父類和繼承該父類的一個或多個子類對某些方法的重寫,多個子類對同一方法的重寫可以表現出不同的行為。

public class Wine {
    private String name;
    
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Wine(){
    }
    
    public String drink(){
        return "喝的是 " + getName();
    }
    
    /**
     * 重寫toString()
     */
    public String toString(){
        return null;
    }
}

public class JNC extends Wine{
    public JNC(){
        setName("JNC");
    }
    
    /**
     * 重寫父類方法,實現多型
     */
    public String drink(){
        return "喝的是 " + getName();
    }
    
    /**
     * 重寫toString()
     */
    public String toString(){
        return "Wine : " + getName();
    }
}

public class JGJ extends Wine{
    public JGJ(){
        setName("JGJ");
    }
    
    /**
     * 重寫父類方法,實現多型
     */
    public String drink(){
        return "喝的是 " + getName();
    }
    
    /**
     * 重寫toString()
     */
    public String toString(){
        return "Wine : " + getName();
    }
}

public class Test {
    public static void main(String[] args) {
        //定義父類陣列
        Wine[] wines = new Wine[2];
        //定義兩個子類
        JNC jnc = new JNC();
        JGJ jgj = new JGJ();
        
        //父類引用子類物件
        wines[0] = jnc;
        wines[1] = jgj;
        
        for(int i = 0 ; i < 2 ; i++){
            System.out.println(wines[i].toString() + "--" + wines[i].drink());
        }
        System.out.println("-------------------------------");

    }
}
OUTPUT:
Wine : JNC--喝的是 JNC
Wine : JGJ--喝的是 JGJ
-------------------------------

在上面的程式碼中JNC、JGJ繼承Wine,並且重寫了drink()、toString()方法,程式執行結果是呼叫子類中方法,輸出JNC、JGJ的名稱,這就是多型的表現。不同的物件可以執行相同的行為,但是他們都需要通過自己的實現方式來執行,這就要得益於向上轉型了。

我們都知道所有的類都繼承自超類Object,toString()方法也是Object中方法,當我們這樣寫時:

1
2
3
Object o = new JGJ();

  System.out.println(o.toString());

輸出的結果是Wine : JGJ。

Object、Wine、JGJ三者繼承鏈關係是:JGJ—>Wine—>Object。所以我們可以這樣說:當子類重寫父類的方法被呼叫時,只有物件繼承鏈中的最末端的方法才會被呼叫。但是注意如果這樣寫:

Object o = new JGJ();

      System.out.println(o.toString());

輸出的結果應該是Null,因為JGJ並不存在於該物件繼承鏈中。

所以基於繼承實現的多型可以總結如下:對於引用子類的父類型別,在處理該引用時,它適用於繼承該父類的所有子類,子類物件的不同,對方法的實現也就不同,執行相同動作產生的行為也就不同。

如果父類是抽象類,那麼子類必須要實現父類中所有的抽象方法,這樣該父類所有的子類一定存在統一的對外介面,但其內部的具體實現可以各異。這樣我們就可以使用頂層類提供的統一介面來處理該層次的方法。

2.2.2、基於介面實現的多型

繼承是通過重寫父類的同一方法的幾個不同子類來體現的,那麼就可就是通過實現介面並覆蓋介面中同一方法的幾不同的類體現的。

在介面的多型中,指向介面的引用必須是指定這實現了該介面的一個類的例項程式,在執行時,根據物件引用的實際型別來執行對應的方法。

繼承都是單繼承,只能為一組相關的類提供一致的服務介面。但是介面可以是多繼承多實現,它能夠利用一組相關或者不相關的介面進行組合與擴充,能夠對外提供一致的服務介面。所以它相對於繼承來說有更好的靈活性。

三、經典例項。
通過上面的講述,可以說是對多型有了一定的瞭解。現在趁熱打鐵,看一個例項。該例項是有關多型的經典例子,摘自:http://blog.csdn.net/thinkGhoster/archive/2008/04/19/2307001.aspx。

public class A {
    public String show(D obj) {
        return ("A and D");
    }

    public String show(A obj) {
        return ("A and A");
    } 

}

public class B extends A{
    public String show(B obj){
        return ("B and B");
    }
    
    public String show(A obj){
        return ("B and A");
    } 
}

public class C extends B{

}

public class D extends B{

}

public class Test {
    public static void main(String[] args) {
        A a1 = new A();
        A a2 = new B();
        B b = new B();
        C c = new C();
        D d = new D();
        
        System.out.println("1--" + a1.show(b));
        System.out.println("2--" + a1.show(c));
        System.out.println("3--" + a1.show(d));
        System.out.println("4--" + a2.show(b));
        System.out.println("5--" + a2.show(c));
        System.out.println("6--" + a2.show(d));
        System.out.println("7--" + b.show(b));
        System.out.println("8--" + b.show(c));
        System.out.println("9--" + b.show(d));      
    }
}
1--A and A
2--A and A
3--A and D
4--B and A
5--B and A
6--A and D
7--B and B
8--B and B
9--A and D

①②③比較好理解,一般不會出錯。④⑤就有點糊塗了,為什麼輸出的不是"B and B”呢?!!先來回顧一下多型性。

執行時多型性是面向物件程式設計程式碼重用的一個最強大機制,Java多型性的概念也可以被說成“一個介面,多個方法”。Java實現執行時多型性的基礎是動態方法排程,它是一種在執行時而不是在編譯期呼叫過載方法的機制。

方法的重寫Overriding和過載Overloading是Java多型性的不同表現。重寫Overriding是父類與子類之間多型性的一種表現,過載Overloading是一個類中多型性的一種表現。如果在子類中定義某方法與其父類有相同的名稱和引數,我們說該方法被重寫(Overriding)。子類的物件使用這個方法時,將呼叫子類中的定義,對它而言,父類中的定義如同被“遮蔽”了。如果在一個類中定義了多個同名的方法,它們或有不同的引數個數或有不同的引數型別,則稱為方法的過載(Overloading)。Overloaded的方法是可以改變返回值的型別。方法的重寫Overriding和過載Overloading是Java多型性的不同表現。重寫Overriding是父類與子類之間多型性的一種表現,過載Overloading是一個類中Java多型性的一種表現。如果在子類中定義某方法與其父類有相同的名稱和引數,我們說該方法被重寫 (Overriding)。子類的物件使用這個方法時,將呼叫子類中的定義,對它而言,父類中的定義如同被“遮蔽”了。如果在一個類中定義了多個同名的方法,它們或有不同的引數個數或有不同的引數型別,則稱為方法的過載(Overloading)。Overloaded的方法是可以改變返回值的型別。

當超類物件引用變數引用子類物件時,被引用物件的型別而不是引用變數的型別決定了呼叫誰的成員方法,但是這個被呼叫的方法必須是在超類中定義過的,也就是說被子類覆蓋的方法。 (但是如果強制把超類轉換成子類的話,就可以呼叫子類中新新增而超類沒有的方法了。)

好了,先溫習到這裡,言歸正傳!實際上這裡涉及方法呼叫的優先問題 ,優先順序由高到低依次為:this.show(O)、super.show(O)、this.show((super)O)、super.show((super)O)。讓我們來看看它是怎麼工作的。

比如④,a2.show(b),a2是一個引用變數,型別為A,則this為a2,b是B的一個例項,於是它到類A裡面找show(B obj)方法,沒有找到,於是到A的super(超類)找,而A沒有超類,因此轉到第三優先順序this.show((super)O),this仍然是a2,這裡O為B,(super)O即(super)B即A,因此它到類A裡面找show(A obj)的方法,類A有這個方法,但是由於a2引用的是類B的一個物件,B覆蓋了A的show(A obj)方法,因此最終鎖定到類B的show(A obj),輸出為"B and A”。

再比如⑧,b.show©,b是一個引用變數,型別為B,則this為b,c是C的一個例項,於是它到類B找show(C obj)方法,沒有找到,轉而到B的超類A裡面找,A裡面也沒有,因此也轉到第三優先順序this.show((super)O),this為b,O為C,(super)O即(super)C即B,因此它到B裡面找show(B obj)方法,找到了,由於b引用的是類B的一個物件,因此直接鎖定到類B的show(B obj),輸出為"B and B”。

按照上面的方法,可以正確得到其他的結果。

問題還要繼續,現在我們再來看上面的分析過程是怎麼體現出藍色字型那句話的內涵的。它說:當超類物件引用變數引用子類物件時,被引用物件的型別而不是引用變數的型別決定了呼叫誰的成員方法,但是這個被呼叫的方法必須是在超類中定義過的,也就是說被子類覆蓋的方法。還是拿a2.show(b)來說吧。

a2是一個引用變數,型別為A,它引用的是B的一個物件,因此這句話的意思是由B來決定呼叫的是哪個方法。因此應該呼叫B的show(B obj)從而輸出"B and B”才對。但是為什麼跟前面的分析得到的結果不相符呢?!問題在於我們不要忽略了藍色字型的後半部分,那裡特別指明:這個被呼叫的方法必須是在超類中定義過的,也就是被子類覆蓋的方法。B裡面的show(B obj)在超類A中有定義嗎?沒有!那就更談不上被覆蓋了。實際上這句話隱藏了一條資訊:它仍然是按照方法呼叫的優先順序來確定的。它在類A中找到了show(A obj),如果子類B沒有覆蓋show(A obj)方法,那麼它就呼叫A的show(A obj)(由於B繼承A,雖然沒有覆蓋這個方法,但從超類A那裡繼承了這個方法,從某種意義上說,還是由B確定呼叫的方法,只是方法是在A中實現而已);現在子類B覆蓋了show(A obj),因此它最終鎖定到B的show(A obj)。這就是那句話的意義所在,到這裡,我們可以清晰的理解Java的多型性了。。

參考資料:http://blog.csdn.net/thinkGhoster/archive/2008/04/19/2307001.aspx。

百度文庫:http://wenku.baidu.com/view/73f66f92daef5ef7ba0d3c03.html

補::
多型存在的三個必要條件

一、要有繼承;

二、要有重寫;

三、父類引用指向子類物件。

多型的類別

編譯期多型(靜態多型,早期繫結)和執行期多型(後期繫結);

繫結:將一個方法呼叫同一個方法主體關聯起來被稱做繫結。

前期繫結:在程式執行前繫結(由編譯器和連線程式實現),稱作前期繫結。它是面向過程的語言中不需要選擇就預設的繫結方式。列入C只有一種方法呼叫,那就是前期繫結。

後期繫結:在執行時根據物件的型別進行繫結。後期繫結也稱作動態繫結或執行時繫結。它通過某種特殊機制實現,即程式一直不知道物件的型別,但是方法呼叫機制呼叫機制能找到正確的方法體,並加以呼叫。

Java中除了static 方法和final 方法(private 屬於final 方法)之外,其他所有方法都是後期繫結。這意味著通常情況下,我們不必判定是否應該進行後期繫結–它會自動發生。

多型的好處:

1.可替換性(substitutability)。多型對已存在程式碼具有可替換性。例如,多型對圓Circle類工作,對其他任何圓形幾何體,如圓環,也同樣工作。
2.可擴充性(extensibility)。多型對程式碼具有可擴充性。增加新的子類不影響已存在類的多型性、繼承性,以及其他特性的執行和操作。實際上新加子類更容易獲得多型功能。例如,在實現了圓錐、半圓錐以及半球體的多型基礎上,很容易增添球體類的多型性。
3.介面性(interface-ability)。多型是超類通過方法簽名,向子類提供了一個共同介面,由子類來完善或者覆蓋它而實現的。如圖8.3所示。圖中超類Shape規定了兩個實現多型的介面方法,computeArea()以及computeVolume()。子類,如Circle和Sphere為了實現多型,完善或者覆蓋這兩個介面方法。
4.靈活性(flexibility)。它在應用中體現了靈活多樣的操作,提高了使用效率。
5.簡化性(simplicity)。多型簡化對應用軟體的程式碼編寫和修改過程,尤其在處理大量物件的運算和操作時,這個特點尤為突出和重要。