鍍金池/ 教程/ Java/ 理解 Java 的三大特性之多態(tài)
Java 集合細(xì)節(jié)(四):保持 compareTo 和 equals 同步
Iterator
使用序列化實(shí)現(xiàn)對(duì)象的拷貝
fail-fast 機(jī)制
關(guān)鍵字 final
Vector
HashTable
Java 集合細(xì)節(jié)(一):請(qǐng)為集合指定初始容量
強(qiáng)制類型轉(zhuǎn)換
數(shù)組之一:認(rèn)識(shí) JAVA 數(shù)組
Java 集合細(xì)節(jié)(三):subList 的缺陷
hashCode
ArrayList
數(shù)組之二
List 總結(jié)
LinkedList
Java 提高篇(九)—–實(shí)現(xiàn)多重繼承
Java 的四舍五入
關(guān)鍵字 static
理解 Java 的三大特性之多態(tài)
抽象類與接口
集合大家族
異常(二)
Java 集合細(xì)節(jié)(二):asList 的缺陷
Map 總結(jié)
TreeSet
equals() 方法總結(jié)
Java 提高篇(十)—–詳解匿名內(nèi)部類
HashMap
Stack
詳解內(nèi)部類
TreeMap
異常(一)
詳解 Java 定時(shí)任務(wù)
HashSet
字符串
理解 Java 的三大特性之繼承
理解 Java 的三大特性之封裝
代碼塊

理解 Java 的三大特性之多態(tài)

面向?qū)ο缶幊逃腥筇匦裕悍庋b、繼承、多態(tài)。

封裝隱藏了類的內(nèi)部實(shí)現(xiàn)機(jī)制,可以在不影響使用的情況下改變類的內(nèi)部結(jié)構(gòu),同時(shí)也保護(hù)了數(shù)據(jù)。對(duì)外界而已它的內(nèi)部細(xì)節(jié)是隱藏的,暴露給外界的只是它的訪問(wèn)方法。

繼承是為了重用父類代碼。兩個(gè)類若存在 IS-A 的關(guān)系就可以使用繼承。,同時(shí)繼承也為實(shí)現(xiàn)多態(tài)做了鋪墊。那么什么是多態(tài)呢?多態(tài)的實(shí)現(xiàn)機(jī)制又是什么?請(qǐng)看我一一為你揭開:

所謂多態(tài)就是指程序中定義的引用變量所指向的具體類型和通過(guò)該引用變量發(fā)出的方法調(diào)用在編程時(shí)并不確定,而是在程序運(yùn)行期間才確定,即一個(gè)引用變量倒底會(huì)指向哪個(gè)類的實(shí)例對(duì)象,該引用變量發(fā)出的方法調(diào)用到底是哪個(gè)類中實(shí)現(xiàn)的方法,必須在由程序運(yùn)行期間才能決定。因?yàn)樵诔绦蜻\(yùn)行時(shí)才確定具體的類,這樣,不用修改源程序代碼,就可以讓引用變量綁定到各種不同的類實(shí)現(xiàn)上,從而導(dǎo)致該引用調(diào)用的具體方法隨之改變,即不修改程序代碼就可以改變程序運(yùn)行時(shí)所綁定的具體代碼,讓程序可以選擇多個(gè)運(yùn)行狀態(tài),這就是多態(tài)性。

比如你是一個(gè)酒神,對(duì)酒情有獨(dú)鐘。某日回家發(fā)現(xiàn)桌上有幾個(gè)杯子里面都裝了白酒,從外面看我們是不可能知道這是些什么酒,只有喝了之后才能夠猜出來(lái)是何種酒。你一喝,這是劍南春、再喝這是五糧液、再喝這是酒鬼酒……在這里我們可以描述成如下:

酒 a = 劍南春

酒 b = 五糧液

酒 c = 酒鬼酒

這里所表現(xiàn)的的就是多態(tài)。劍南春、五糧液、酒鬼酒都是酒的子類,我們只是通過(guò)酒這一個(gè)父類就能夠引用不同的子類,這就是多態(tài)——我們只有在運(yùn)行的時(shí)候才會(huì)知道引用變量所指向的具體實(shí)例對(duì)象。

誠(chéng)然,要理解多態(tài)我們就必須要明白什么是“向上轉(zhuǎn)型”。在繼承中我們簡(jiǎn)單介紹了向上轉(zhuǎn)型,這里就在啰嗦下:在上面的喝酒例子中,酒(Win)是父類,劍南春(JNC)、五糧液(WLY)、酒鬼酒(JGJ)是子類。我們定義如下代碼:

JNC a = new JNC();

對(duì)于這個(gè)代碼我們非常容易理解無(wú)非就是實(shí)例化了一個(gè)劍南春的對(duì)象嘛!但是這樣呢?

Wine a = new JNC();

在這里我們這樣理解,這里定義了一個(gè) Wine 類型的 a,它指向 JNC 對(duì)象實(shí)例。由于 JNC 是繼承與 Wine,所以JNC可以自動(dòng)向上轉(zhuǎn)型為 Wine,所以 a 是可以指向 JNC 實(shí)例對(duì)象的。這樣做存在一個(gè)非常大的好處,在繼承中我們知道子類是父類的擴(kuò)展,它可以提供比父類更加強(qiáng)大的功能,如果我們定義了一個(gè)指向子類的父類引用類型,那么它除了能夠引用父類的共性外,還可以使用子類強(qiáng)大的功能。

但是向上轉(zhuǎn)型存在一些缺憾,那就是它必定會(huì)導(dǎo)致一些方法和屬性的丟失,而導(dǎo)致我們不能夠獲取它們。所以父類類型的引用可以調(diào)用父類中定義的所有屬性和方法,對(duì)于只存在與子類中的方法和屬性它就望塵莫及了。


    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 子類重載父類方法
         *        父類中不存在該方法,向上轉(zhuǎn)型后,父類是不能引用該方法的
         * @param a
         * @return void
         */
        public void fun1(String a){
            System.out.println("JNC 的 Fun1...");
            fun2();
        }

        /**
         * 子類重寫父類方法
         * 指向子類的父類引用調(diào)用fun2時(shí),必定是調(diào)用該方法
         */
        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...

從程序的運(yùn)行結(jié)果中我們發(fā)現(xiàn),a.fun1()首先是運(yùn)行父類 Wine 中的 fun1().然后再運(yùn)行子類 JNC 中的 fun2()。

分析:在這個(gè)程序中子類 JNC 重載了父類 Wine 的方法 fun1(),重寫 fun2(),而且重載后的 fun1(String a) 與 fun1() 不是同一個(gè)方法,由于父類中沒有該方法,向上轉(zhuǎn)型后會(huì)丟失該方法,所以執(zhí)行 JNC 的 Wine 類型引用是不能引用 fun1(String a) 方法。而子類 JNC 重寫了 fun2(),那么指向 JNC 的 Wine 引用會(huì)調(diào)用 JNC 中 fun2() 方法。

所以對(duì)于多態(tài)我們可以總結(jié)如下:

指向子類的父類引用由于向上轉(zhuǎn)型了,它只能訪問(wèn)父類中擁有的方法和屬性,而對(duì)于子類中存在而父類中不存在的方法,該引用是不能使用的,盡管是重載該方法。若子類重寫了父類中的某些方法,在調(diào)用該些方法的時(shí)候,必定是使用子類中定義的這些方法(動(dòng)態(tài)連接、動(dòng)態(tài)調(diào)用)。

對(duì)于面向?qū)ο蠖眩鄳B(tài)分為編譯時(shí)多態(tài)和運(yùn)行時(shí)多態(tài)。其中編輯時(shí)多態(tài)是靜態(tài)的,主要是指方法的重載,它是根據(jù)參數(shù)列表的不同來(lái)區(qū)分不同的函數(shù),通過(guò)編輯之后會(huì)變成兩個(gè)不同的函數(shù),在運(yùn)行時(shí)談不上多態(tài)。而運(yùn)行時(shí)多態(tài)是動(dòng)態(tài)的,它是通過(guò)動(dòng)態(tài)綁定來(lái)實(shí)現(xiàn)的,也就是我們所說(shuō)的多態(tài)性。

多態(tài)的實(shí)現(xiàn)

2.1實(shí)現(xiàn)條件

在剛剛開始就提到了繼承在為多態(tài)的實(shí)現(xiàn)做了準(zhǔn)備。子類 Child 繼承父類 Father,我們可以編寫一個(gè)指向子類的父類類型引用,該引用既可以處理父類Father對(duì)象,也可以處理子類 Child 對(duì)象,當(dāng)相同的消息發(fā)送給子類或者父類對(duì)象時(shí),該對(duì)象就會(huì)根據(jù)自己所屬的引用而執(zhí)行不同的行為,這就是多態(tài)。即多態(tài)性就是相同的消息使得不同的類做出不同的響應(yīng)。

Java 實(shí)現(xiàn)多態(tài)有三個(gè)必要條件:繼承、重寫、向上轉(zhuǎn)型。

繼承:在多態(tài)中必須存在有繼承關(guān)系的子類和父類。

重寫:子類對(duì)父類中某些方法進(jìn)行重新定義,在調(diào)用這些方法時(shí)就會(huì)調(diào)用子類的方法。

向上轉(zhuǎn)型:在多態(tài)中需要將子類的引用賦給父類對(duì)象,只有這樣該引用才能夠具備技能調(diào)用父類的方法和子類的方法。

只有滿足了上述三個(gè)條件,我們才能夠在同一個(gè)繼承結(jié)構(gòu)中使用統(tǒng)一的邏輯實(shí)現(xiàn)代碼處理不同的對(duì)象,從而達(dá)到執(zhí)行不同的行為。

對(duì)于 Java 而言,它多態(tài)的實(shí)現(xiàn)機(jī)制遵循一個(gè)原則:當(dāng)超類對(duì)象引用變量引用子類對(duì)象時(shí),被引用對(duì)象的類型而不是引用變量的類型決定了調(diào)用誰(shuí)的成員方法,但是這個(gè)被調(diào)用的方法必須是在超類中定義過(guò)的,也就是說(shuō)被子類覆蓋的方法。

2.2實(shí)現(xiàn)形式

在 Java 中有兩種形式可以實(shí)現(xiàn)多態(tài)。繼承和接口。

2.2.1.基于繼承實(shí)現(xiàn)的多態(tài)

基于繼承的實(shí)現(xiàn)機(jī)制主要表現(xiàn)在父類和繼承該父類的一個(gè)或多個(gè)子類對(duì)某些方法的重寫,多個(gè)子類對(duì)同一方法的重寫可以表現(xiàn)出不同的行為。


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

        /**
         * 重寫父類方法,實(shí)現(xiàn)多態(tài)
         */
        public String drink(){
            return "喝的是 " + getName();
        }

        /**
         * 重寫toString()
         */
        public String toString(){
            return "Wine : " + getName();
        }
    }

    public class JGJ extends Wine{
        public JGJ(){
            setName("JGJ");
        }

        /**
         * 重寫父類方法,實(shí)現(xiàn)多態(tài)
         */
        public String drink(){
            return "喝的是 " + getName();
       }

        /**
         * 重寫toString()
         */
        public String toString(){
            return "Wine : " + getName();
        }
    }

    public class Test {
        public static void main(String[] args) {
            //定義父類數(shù)組
            Wine[] wines = new Wine[2];
            //定義兩個(gè)子類
            JNC jnc = new JNC();
            JGJ jgj = new JGJ();

            //父類引用子類對(duì)象
            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() 方法,程序運(yùn)行結(jié)果是調(diào)用子類中方法,輸出 JNC、JGJ 的名稱,這就是多態(tài)的表現(xiàn)。不同的對(duì)象可以執(zhí)行相同的行為,但是他們都需要通過(guò)自己的實(shí)現(xiàn)方式來(lái)執(zhí)行,這就要得益于向上轉(zhuǎn)型了。

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


    Object o = new JGJ();

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

輸出的結(jié)果是 Wine : JGJ。

Object、Wine、JGJ 三者繼承鏈關(guān)系是:JGJ—>Wine—>Object。所以我們可以這樣說(shuō):當(dāng)子類重寫父類的方法被調(diào)用時(shí),只有對(duì)象繼承鏈中的最末端的方法才會(huì)被調(diào)用。但是注意如果這樣寫:


    Object o = new Wine();

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

輸出的結(jié)果應(yīng)該是 Null,因?yàn)?JGJ 并不存在于該對(duì)象繼承鏈中。

所以基于繼承實(shí)現(xiàn)的多態(tài)可以總結(jié)如下:對(duì)于引用子類的父類類型,在處理該引用時(shí),它適用于繼承該父類的所有子類,子類對(duì)象的不同,對(duì)方法的實(shí)現(xiàn)也就不同,執(zhí)行相同動(dòng)作產(chǎn)生的行為也就不同。

如果父類是抽象類,那么子類必須要實(shí)現(xiàn)父類中所有的抽象方法,這樣該父類所有的子類一定存在統(tǒng)一的對(duì)外接口,但其內(nèi)部的具體實(shí)現(xiàn)可以各異。這樣我們就可以使用頂層類提供的統(tǒng)一接口來(lái)處理該層次的方法。

2.2.2.基于接口實(shí)現(xiàn)的多態(tài)

繼承是通過(guò)重寫父類的同一方法的幾個(gè)不同子類來(lái)體現(xiàn)的,那么就可就是通過(guò)實(shí)現(xiàn)接口并覆蓋接口中同一方法的幾不同的類體現(xiàn)的。

在接口的多態(tài)中,指向接口的引用必須是指定這實(shí)現(xiàn)了該接口的一個(gè)類的實(shí)例程序,在運(yùn)行時(shí),根據(jù)對(duì)象引用的實(shí)際類型來(lái)執(zhí)行對(duì)應(yīng)的方法。

繼承都是單繼承,只能為一組相關(guān)的類提供一致的服務(wù)接口。但是接口可以是多繼承多實(shí)現(xiàn),它能夠利用一組相關(guān)或者不相關(guān)的接口進(jìn)行組合與擴(kuò)充,能夠?qū)ν馓峁┮恢碌姆?wù)接口。所以它相對(duì)于繼承來(lái)說(shuō)有更好的靈活性。

經(jīng)典實(shí)例

通過(guò)上面的講述,可以說(shuō)是對(duì)多態(tài)有了一定的了解?,F(xiàn)在趁熱打鐵,看一個(gè)實(shí)例。該實(shí)例是有關(guān)多態(tài)的經(jīng)典例子,摘自: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));      
        }
    }
          運(yùn)行結(jié)果:

    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

在這里看結(jié)果1、2、3還好理解,從4開始就開始糊涂了,對(duì)于4來(lái)說(shuō)為什么輸出不是“B and B”呢?

首先我們先看一句話:當(dāng)超類對(duì)象引用變量引用子類對(duì)象時(shí),被引用對(duì)象的類型而不是引用變量的類型決定了調(diào)用誰(shuí)的成員方法,但是這個(gè)被調(diào)用的方法必須是在超類中定義過(guò)的,也就是說(shuō)被子類覆蓋的方法。這句話對(duì)多態(tài)進(jìn)行了一個(gè)概括。其實(shí)在繼承鏈中對(duì)象方法的調(diào)用存在一個(gè)優(yōu)先級(jí):this.show(O)、super.show(O)、this.show((super)O)、super.show((super)O)。

分析:

從上面的程序中我們可以看出 A、B、C、D 存在如下關(guān)系。

首先我們分析5,a2.show(c),a2 是 A 類型的引用變量,所以 this 就代表了 A,a2.show(c),它在 A 類中找發(fā)現(xiàn)沒有找到,于是到 A 的超類中找(super),由于 A 沒有超類(Object除外),所以跳到第三級(jí),也就是 this.show((super)O),C 的超類有 B、A,所以 (super)O 為 B、A,this 同樣是 A,這里在 A 中找到了 show(A obj),同時(shí)由于 a2 是 B 類的一個(gè)引用且 B 類重寫了 show(A obj),因此最終會(huì)調(diào)用子類B類的 show(A obj) 方法,結(jié)果也就是 B and A。

按照同樣的方法我也可以確認(rèn)其他的答案。

方法已經(jīng)找到了但是我們這里還是存在一點(diǎn)疑問(wèn),我們還是來(lái)看這句話:當(dāng)超類對(duì)象引用變量引用子類對(duì)象時(shí),被引用對(duì)象的類型而不是引用變量的類型決定了調(diào)用誰(shuí)的成員方法,但是這個(gè)被調(diào)用的方法必須是在超類中定義過(guò)的,也就是說(shuō)被子類覆蓋的方法。這我們用一個(gè)例子來(lái)說(shuō)明這句話所代表的含義:a2.show(b);

這里 a2 是引用變量,為 A 類型,它引用的是 B 對(duì)象,因此按照上面那句話的意思是說(shuō)有B來(lái)決定調(diào)用誰(shuí)的方法,所以 a2.show(b) 應(yīng)該要調(diào)用B中的 show(B obj),產(chǎn)生的結(jié)果應(yīng)該是“B and B”,但是為什么會(huì)與前面的運(yùn)行結(jié)果產(chǎn)生差異呢?這里我們忽略了后面那句話“但是這兒被調(diào)用的方法必須是在超類中定義過(guò)的”,那么 show(B obj) 在 A 類中存在嗎?根本就不存在!所以這句話在這里不適用?那么難道是這句話錯(cuò)誤了?非也!其實(shí)這句話還隱含這這句話:它仍然要按照繼承鏈中調(diào)用方法的優(yōu)先級(jí)來(lái)確認(rèn)。所以它才會(huì)在 A 類中找到 show(A obj),同時(shí)由于 B 重寫了該方法所以才會(huì)調(diào)用 B 類中的方法,否則就會(huì)調(diào)用 A 類中的方法。

所以多態(tài)機(jī)制遵循的原則概括為:當(dāng)超類對(duì)象引用變量引用子類對(duì)象時(shí),被引用對(duì)象的類型而不是引用變量的類型決定了調(diào)用誰(shuí)的成員方法,但是這個(gè)被調(diào)用的方法必須是在超類中定義過(guò)的,也就是說(shuō)被子類覆蓋的方法,但是它仍然要根據(jù)繼承鏈中方法調(diào)用的優(yōu)先級(jí)來(lái)確認(rèn)方法,該優(yōu)先級(jí)為:this.show(O)、super.show(O)、this.show((super)O)、super.show((super)O)。

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

百度文庫(kù):http://wenku.baidu.com/view/73f66f92daef5ef7ba0d3c03.html

在這里面向?qū)ο蟮娜筇匦砸呀?jīng)介紹完成,下一步繼續(xù)是 java 基礎(chǔ)部分—鞏固基礎(chǔ),提高技術(shù),不懼困難,攀登高峰?。。。。?!