1. 程式人生 > >Java面向物件三大特性一:多型詳解

Java面向物件三大特性一:多型詳解

多型

多型(polymorphism):指的是同一個方法呼叫,由於物件不同可能會有不同的行為。現實生活中,同一個方法,具體實現會完全不同。

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

多型的要點

  • 多型是方法的多型,不是屬性的多型(多型和屬性無關)

  • 多型的存在要有3個必要條件:繼承、方法重寫、父類引用指向子類物件

  • 父類引用指向子類物件後,用該父類引用呼叫子類重寫的方法,多型就出現了。

多型的型別

  • 編譯時多型:編譯時多型是靜態的,主要是指方法的過載,根據引數列表的不同來區分不同的方法呼叫,通過編譯之後會變成兩個不同的方法,並不能真正算作多型。

  • 執行時多型:主要運用到動態繫結。就是在執行時動態的確定呼叫的是哪一個方法。

物件的轉型

父類引用指向子類物件,我們稱這個過程為向上轉型,屬於自動型別轉換。

向上轉型後的父類引用變數只能呼叫它編譯型別的方法,不能呼叫它執行時型別的方法。這時,我們就需要進行型別的強制轉換,我們稱之為向下轉型。

多型的好處

  • 可替換性(substitutability):多型對已存程式碼具有可替換性。例如,draw函式對圓Circle類工作,對其他任何圓形幾何體,如圓環,也同樣工作。

  • 可擴充性(extensibility):多型對程式碼具有可擴充性,增加新的子類不影響已存在類的多型性、繼承性以及其他特性的執行和操作,實際上新加子類更容易獲得多型的功能。例如,在實現了圓錐、半圓錐以及半球體的多型基礎上,很容易增添球體類的多型性。

  • 介面性(interface-ability):多型是超類通過方法簽名,向子類提供了一個共同的介面,由子類來完善或者覆蓋它而實現的。

  • 靈活性(flexibility)

    :在應用中體現了靈活多樣的操作,提高了使用效率

  • 簡化性(simplicity):多型簡化對應用軟體的程式碼編寫和修改過程,尤其在處理大量物件的運算和操作時,這個特點尤為突出和重要。

示例

編譯時多型示例

方法過載都是編譯時多型。根據實際引數的資料型別、個數和次序,Java在編譯時能夠確定執行過載方法中的哪一個。

方法覆蓋表現出兩種多型性,當物件引用本類例項時,為編譯時多型,否則為執行時多型。

程式碼示例如下

public class Test1 {

    public static void main(String[] args) {
        Person p = new Person();
        Man m = new Man();
        System.out.println(p.toString()); //編譯時多型,執行Person類的toString()
        System.out.println(m.toString()); //編譯時多型,執行Man類的toString()
    }
}

class Person {
    public String toString() {
        String name = "Person";
        return name;
    }
}

class Man extends Person {
    public String toString() {
        String name = "Man";
        return name;
    }
}

輸出結果為:

Person
Man

可以看出,當物件引用本類例項時,呼叫本類對應的方法,這就是編譯時多型。

執行時多型示例

當以下父類物件p引用子類例項時,p.toString)執行哪個類的toString()方法呢,在編譯時編譯器無法判定

Person p = new Man();   
p.toString();

程式執行時,Java從例項所屬的類開始尋找匹配的方法執行,如果當前類中沒有匹配的方法,則沿著繼承關係逐層向上,依次在父類或各祖先類中尋找匹配方法,直到Object類。尋找p.toString()匹配執行方法的過程如下圖所示。
這裡寫圖片描述
在看一個經典例子,參考文章:http://www.cnblogs.com/chenssy/p/3372798.html

程式碼示例如下

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

這個結果開始沒有完全答對,在結合大神chenssy關於這塊的分析後,終於弄懂了,將這些結果逐一分析,如有不對,歡迎拍磚~

這裡有句話是這樣的:當超類物件引用變數引用子類物件時,是由被引用物件的型別而不是引用變數的型別決定了呼叫誰的成員方法,但是這個被呼叫的方法必須是在超類中定義過的,也就是說被子類覆蓋的方法。例如System.out.println("4--" + a2.show(b))這行程式碼,其中a2為超類物件引用變數,new B()為子類物件,即a2引用new B(),那麼a2.show(b)是由被引用物件的型別(即B類)來決定呼叫誰的成員方法,根據上面第一個示例中提到的執行時多型中有提到:Java從例項所屬的類開始尋找匹配的方法執行,那麼先從B類中找有show(b)匹配的方法。

其實在繼承鏈中物件方法的呼叫存在一個優先順序:this.show(O)、super.show(O)、this.show((super)O)、super.show((super)O)。

那麼,按照這個優先順序我們來逐一分析每一個輸出結果:

  • 輸出(1–A and A):a1.show(b),a1是A型別的引用變數,所以this就代表了A,a1.show(b),B的超類是A,所以在A中找show(A obj),因此結果就是A and A

  • 輸出(2–A and A):a1.show(c),a1是A型別的引用變數,所以this就代表了A,a1.show(b),C的超類有A和B,所以在A中找show(A obj),因此結果就是A and A

  • 輸出(3–A and D):a1.show(d),a1是A型別的引用變數,所以this就代表了A,a1.show(d),在A中找show(D obj),因此結果就是A and D

  • 輸出(4–B and A):a2.show(b),a2是A型別的引用變數,所以this就代表了A,a2.show(b),它在A類中找發現沒有找到,於是到A的超類中找(super),由於A沒有超類(Object除外),所以跳到第三級,也就是this.show((super)O),B的超類是A,所以(super)O為A,this同樣是A,這裡在A中找到了show(A obj),同時由於a2是B類的一個引用且B類重寫了show(A obj),因此最終會呼叫子類B類的show(A obj)方法,結果也就是B and A。按照上面給出的繼承鏈中物件方法的呼叫存在一個優先順序為:A.show(B obj) –> A.show(B的超類即A) –> 父類引用指向子類物件(B.show(A))

  • 輸出(5–B and A):a2.show(c),a2是A型別的引用變數,所以this就代表了A,a2.show(c),它在A類中找發現沒有找到,於是到A的超類中找(super),由於A沒有超類(Object除外),所以跳到第三級,也就是this.show((super)O),C的超類有B、A,所以(super)O為B、A,this同樣是A,這裡在A中找到了show(A obj),同時由於a2是B類的一個引用且B類重寫了show(A obj),因此最終會呼叫子類B類的show(A obj)方法,結果也就是B and A。按照上面給出的繼承鏈中物件方法的呼叫存在一個優先順序為:A.show(C obj) –> A.show(C的超類即B、A) –> 父類引用指向子類物件(B.show(A))

  • 輸出(6–A and D):a2.show(d),a2是A型別的引用變數,所以this就代表了A,a2.show(d),這裡在A中找到了show(D obj)方法,結果也就是A和D。

  • 輸出(7–B and B):b.show(b),b是B型別的引用變數,所以this就代表了B,b.show(b),它在B類中找到了show(B obj)方法,結果也就是B and B

  • 輸出(8–B and B):b.show(c),b是B型別的引用變數,所以this就代表了B,b.show(c),C的超類是B,它在B類中沒找到了show(c)方法,於是到A的超類中找show(c),發現也沒有找到對應的方法,最後在B類中找到了show(B obj)方法,結果也就是B and B。按照上面給出的繼承鏈中物件方法的呼叫存在一個優先順序為:B.show(C obj) –> A.show(C obj) –> B.show(C的超類即B)

  • 輸出(9–A and D):b.show(d),b是B型別的引用變數,所以this就代表了B,b.show(d),它在B類中沒找到了show(d)方法,於是到A的超類中找show(d)方法,最終找到了,結果也就是B and B。按照上面給出的繼承鏈中物件方法的呼叫存在一個優先順序為:B.show(D obj) –> A.show(D obj)