理解java的三大特性之多態(三)

来源:http://www.cnblogs.com/dangzhenjiuhao/archive/2016/02/16/5191563.html
-Advertisement-
Play Games

摘自:http://cmsblogs.com/?p=52 面向對象編程有三大特性:封裝、繼承、多態。 封裝隱藏了類的內部實現機制,可以在不影響使用的情況下改變類的內部結構,同時也保護了數據。對外界而已它的內部細節是隱藏的,暴露給外界的只是它的訪問方法。 繼承是為了重用父類代碼。兩個類若存在IS-A的


摘自:http://cmsblogs.com/?p=52

  

面向對象編程有三大特性:封裝、繼承、多態。

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

      繼承是為了重用父類代碼。兩個類若存在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實例對象的。這樣做存在一個非常大的好處,在繼承中我們知道子類是父類的擴展,它可以提供比父類更加強大的功能,如果我們定義了一個指向子類的父類引用類型,那麼它除了能夠引用父類的共性外,還可以使用子類強大的功能。

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

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中方法,當我們這樣寫時:

Object o = new JGJ();

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

 

      輸出的結果是Wine : JGJ。

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

 

Object o = new Wine();

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

      在這裡看結果1、2、3還好理解,從4開始就開始糊塗了,對於4來說為什麼輸出不是“B and B”呢?

      首先我們先看一句話:當超類對象引用變數引用子類對象時,被引用對象的類型而不是引用變數的類型決定了調用誰的成員方法,但是這個被調用的方法必須是在超類中定義過的,也就是說被子類覆蓋的方法。這句話對多態進行了一個概括。其實在繼承鏈中對象方法的調用存在一個優先順序:this.show(O)、super.show(O)、this.show((super)O)、super.show((super)O)。

      分析:

     從上面的程式中我們可以看出A、B、C、D存在如下關係。

11111  

      首先我們分析5,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。

      按照同樣的方法我也可以確認其他的答案。

      方法已經找到了但是我們這裡還是存在一點疑問,我們還是來看這句話:當超類對象引用變數引用子類對象時,被引用對象的類型而不是引用變數的類型決定了調用誰的成員方法,但是這個被調用的方法必須是在超類中定義過的,也就是說被子類覆蓋的方法。這我們用一個例子來說明這句話所代表的含義:a2.show(b);

      這裡a2是引用變數,為A類型,它引用的是B對象,因此按照上面那句話的意思是說有B來決定調用誰的方法,所以a2.show(b)應該要調用B中的show(B obj),產生的結果應該是“B and B”,但是為什麼會與前面的運行結果產生差異呢?這裡我們忽略了後面那句話“但是這兒被調用的方法必須是在超類中定義過的”,那麼show(B obj)在A類中存在嗎?根本就不存在!所以這句話在這裡不適用?那麼難道是這句話錯誤了?非也!其實這句話還隱含這這句話:它仍然要按照繼承鏈中調用方法的優先順序來確認。所以它才會在A類中找到show(A obj),同時由於B重寫了該方法所以才會調用B類中的方法,否則就會調用A類中的方法。

      所以多態機制遵循的原則概括為:當超類對象引用變數引用子類對象時,被引用對象的類型而不是引用變數的類型決定了調用誰的成員方法,但是這個被調用的方法必須是在超類中定義過的,也就是說被子類覆蓋的方法,但是它仍然要根據繼承鏈中方法調用的優先順序來確認方法,該優先順序為: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

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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 事件模型是被廣泛使用的好東西,但是C++標準庫里沒有現成的,其他實現又複雜或者不優雅,比如需要使用巨集。現在VC11可以用在XP下了,那麼就痛快的拿起C++11提供的先進設施組合出一個輕便的實現吧。 為了達到簡潔的目的,需要放棄一些特性: 1、不支持判斷函數是否已經綁定過(因為std::functio
  • 清除空格的方法是不安全的,部分原因是因為字元中的空格非常多,例如 "addslashes的問題在 於黑客 可以用0xbf27來代替單引號,而addslashes只是將0xbf27修改為0xbf5c27,成為一個有效的多位元組字元,其中的0xbf5c仍會 被看作是單引號,所以addslashes無法成功
  • 以下是小白的爬蟲學習歷程中遇到並解決的一些困難,希望寫出來給後來人,如有疏漏懇請大牛指正,不勝感謝! 首先,我的代碼是這樣的 1 2 3 import requests 4 5 url = 'http://www.acfun.tv/' 6 html = requests.get(url) 7 8 p
  • ArrayList與LinkedList的普通for迴圈遍歷 對於大部分Java程式員朋友們來說,可能平時使用得最多的List就是ArrayList,對於ArrayList的遍歷,一般用如下寫法: public static void main(String[] args) { List<Integ
  • 遲來的年後總結 其實很多時候都想寫兩篇博客來記錄下工作學習中的點點滴滴,不過自從去年10月份後,工作上總是無比的忙,新功能,新項目,需求不明確,需求變動等原因導致在後期長時間的加班和趕工。不過慶幸2015終歸拉下了帷幕。在2015年中雖然累,雖然有段時間無比苦逼,不過也算是學了很多,如 技術上的突破
  • 不管什麼平臺,應用內難免會出現一些消息提示框,下麵就來聊聊我在UWP里用到的消息提示框。 彈窗也可按是否需要用戶操作促發一些邏輯進行分為兩大類。 不需要用戶干涉的一類: MessageDialog:操作簡單,寫起來也省事,想具體瞭解的請參考MSDN 先看看效果 PC上效果: mobile上效果: 再
  • 原文地址:http://blog.sina.com.cn/s/blog_604fb7ae0100x2s7.html 中小企業辦公自動化系統都需要有與微軟辦公軟體連接的功能,如把數據導入到電子錶格、Word等功能。C#.NET在Office方面提供了強大的功能,只要導入 Microsoft.Offic
  • (1/2)類型 普通函數指針不能被賦值為成員函數的地址。 int (*pFunc)(); pFunc是一個函數指針,而int (*)()是類型。 成員函數地址要賦值給成員函數指針。 class Base { public: int func() { return 1; } }; int main()
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...