您好,登錄后才能下訂單哦!
小編給大家分享一下java中多態的案例分析,希望大家閱讀完這篇文章后大所收獲,下面讓我們一起去探討吧!
今天,跟往常一樣踩點來到了公司。坐到自己的工位上打開電腦,"又是搬磚的一天"
。想歸想,還是"熟練"
的打開了 Idea,看了下今天的需求,便敲起了代碼。咦,這些代碼是誰寫的,怎么出現在我的代碼里面,而且還是待提交狀態,我記得我沒寫過呀,饒有興趣的看了看:
這不是多態嗎,誰在我電腦寫的測試,不禁一陣奇怪。
"你看看這會輸出什么結果?"
一陣聲音從身后傳來,因為在思考輸出結果,也沒在意聲音的來源,繼續看了看代碼,便得出結論:
polygon() before cal() square.cal(), border = 2 polygon() after cal() square.square(), border = 4復制代碼
心里想:就這?起碼也是名 Java 開發工程師好嗎,雖然平時搬搬磚,一些基本功還是有的。不禁有點得意了~
"這就是你的答案嗎?看來你也不咋的"
聲音又突然響起,這次我不淡定了,尼瑪!這答案我也是在心里想的好嗎,誰能看得到啊,而且說得話讓人那么想施展一套阿威十八式。"你是誰啊?"
帶著絲微疑惑和憤怒轉過了頭。怎么沒人?容不得我疑惑半分,"小菜,醒醒,你怎么上班時間就睡著了"
上班時間,睡著了?我睜開了眼,看了下周圍環境,原來是夢啊,舒了一口氣。望眼就看到部門主管站在我面前,上班時間睡覺,你是身體不舒服還是咋樣?昨天寫了一堆 bug 沒改,今天又提交什么亂七八糟的東西上去,我看你這個月的績效是不想要的,而且基于你的表現,我也要開始為部門考慮考慮了。
"我不是,我沒有,我也不知道怎么就睡著了,你聽我解釋啊!"
這句話還沒來得及說出口,心里的花我要帶你回家,在那深夜酒吧哪管它是真是假,請你盡情搖擺忘記鐘意的他,你是最迷人噶,你知道嗎
,鬧鈴響了起來,我一下子立起身子,后背微濕,額頂微汗,看了下手機,周六,8點30分,原來那是夢啊!
奇怪,怎么會做那么奇怪的夢,也太嚇人了。然后就想到了夢中的那部分代碼,難道我的結果是錯的嗎?憑著記憶,在電腦上重新敲了出來,運行結果如下:
/* polygon() before cal() square.cal(), border = 0 polygon() after cal() square.square(), border = 4 */復制代碼
square.cal(), border
的結果居然是 0,而不是2。難道我現在連多態都不會了嗎?電腦手機前的你,不知道是否得出了正確答案了呢!不管有沒有,接下來就跟小菜一起來復習一下多態吧!
有些小伙伴疑惑的點可能不止square.cal(), border
的結果是 0,也有為什么不是 square.square(), border = 4
先輸出的疑惑。那么我們就帶著疑惑,整起!
在面向對象的程序設計語言中,多態是繼數據抽象和繼承之后的第三種基本特征。
多態不但能夠改善代碼的組織結構和可讀性,還能夠創建可擴展的程序。多態的作用就是消除類型之間的耦合關系
。
根據里氏代換原則
:任何基類可以出現的地方,子類一定可以出現。
對象既可以作為它自己本身的類型使用,也可以作為它的基類型使用。而這種吧對某個對象的引用視為對其基類型的引用的做法被稱作為 - 向上轉型
。因為父類在子類的上方,子類要引用父類,因此稱為 向上轉型
。
public class Animal { void eat() { System.out.println("Animal eat()"); } }class Monkey extends Animal { void eat() { System.out.println(" Monkey eat()"); } }class test { public static void start(Animal animal) { animal.eat(); } public static void main(String[] args) { Monkey monkey = new Monkey(); start(monkey); } }/* OUTPUT: Monkey eat() */復制代碼
上述 test
類中的 start()
方法接收一個 Animal
的引用,自然也可以接收從 Animal
的導出類。調用eat()
方法的時候,自然而然的使用到 Monkey
中定義的eat()
方法,而不需要做任何的類型轉換。因為從 Monkey
向上轉型到 Animal
只能減少接口,而不會比Animal
的接口更少。
打個不是特別恰當的比方:你父親的財產會繼承給你,而你的財產還是你的,總的來說,你的財產不會比你父親的少。
在 test.start()
方法中,定義傳入的是 Animal
的引用,但是卻傳入Monkey
,這看起來似乎忘記了Monkey
的對象類型,那么為什么不直接把test
類中的方法定義為void start(Monkey monkey)
,這樣看上去難道不會更直觀嗎。
直觀也許是它的優點,但是就會帶來其他問題:Animal
不止只有一個Monkey
的導出類,這個時候來了個pig
,那么是不是就要再定義個方法為void start(Monkey monkey)
,重載用得挺溜嘛小伙子,但是未免太麻煩了。懶惰才是開發人員的天性。
因此這樣就有了多態
的產生
方法調用中分為 靜態綁定
和動態綁定
。何為綁定:將一個方法調用同一個方法主體關聯起來被稱作綁定。
靜態綁定
:又稱為前期綁定。是在程序執行前進行把綁定。我們平時聽到"靜態"的時候,不難免想到static
關鍵字,被static
關鍵字修飾后的變量成為靜態變量,這種變量就是在程序執行前初始化的。前期綁定
是面向過程語言中默認的綁定方式,例如 C 語言只有一種方法調用,那就是前期綁定。引出思考:
public static void start(Animal animal) { animal.eat(); }復制代碼
在start()
方法中傳入的是Animal
的對象引用,如果有多個Animal
的導出類,那么執行eat()
方法的時候如何知道調用哪個方法。如果通過前期綁定
那么是無法實現的。因此就有了后期綁定
。
動態綁定
:又稱為后期綁定
。是在程序運行時根據對象類型進行綁定的,因此又可以稱為運行時綁定
。而 Java 就是根據它自己的后期綁定機制,以便在運行時能夠判斷對象的類型,從而調用正確的方法。小結:
Java 中除了 static
和 final
修飾的方法之外,都是屬于后期綁定
顯然通過動態綁定
來實現多態
是合理的。這樣子我們在開發接口的時候只需要傳入 基類 的引用,從而這些代碼對所有 基類 的 導出類 都可以正確的運行。
其中Monkey
、Pig
、Dog
皆是Animal
的導出類
Animal animal = new Monkey()
看上去不正確的賦值,但是上通過繼承,Monkey
就是一種Animal
,如果我們調用animal.eat()
方法,不了解多態的小伙伴常常會誤以為調用的是Animal
的eat()
方法,但是最終卻是調用了Monkey
自己的eat()
方法。
Animal
作為基類,它的作用就是為導出類建立公用接口。所有從Animal
繼承出去的導出類都可以有自己獨特的實現行為。
有了多態機制,我們可以根據自己的需求對系統添加任意多的新類型,而不需要重載void start(Animal animal)
方法。
在一個設計良好的OOP程序中,大多數或者所有方法都會遵循start()
方法的模型,只與基類接口同行,這樣的程序就是具有可擴展性的,我們可以通過從通用的基類繼承出新的數據類型,從而添加一些功能,那些操縱基類接口的方法就不需要任何改動就可以應用于新類。
我們先來復習一下權限修飾符:
作用域 | 當前類 | 用一個package | 子孫類 | 其他package |
---|---|---|---|---|
public | √ | √ | √ | √ |
protected | √ | √ | √ | × |
default | √ | √ | × | × |
private | √ | × | × | × |
私有方法帶來的失靈:
復習完我們再來看一組代碼:
public class PrivateScope { private void f() { System.out.println("PrivateScope f()"); } public static void main(String[] args) { PrivateScope p = new PrivateOverride(); p.f(); } }class PrivateOverride extends PrivateScope { private void f() { System.out.println("PrivateOverride f()"); } }/* OUTPUT PrivateScope f() */復制代碼
是否感到有點奇怪,為什么這個時候調用的f()
是基類中定義的,而不像上面所述的那樣,通過動態綁定
,從而調用導出類PrivateOverride
中定義的f()
。不知道心細的你是否發現,基類中f()
方法的修飾是private。沒錯,這就是問題所在,PrivateOverride
中定義的f()
方法是一個全新的方法,因為private
的緣故,對子類不可見,自然也不能被重載。
結論:
只有非 private
修飾的方法才可以被覆蓋
我們通過 Idea 寫代碼的時候,重寫的方法頭上可以標注@Override
注解,如果不是重寫的方法,標注@Override
注解就會報錯:
這樣也可以很好的提示我們非重寫方法,而是全新的方法。
域帶來的失靈:
當小伙伴看到這里,就會開始認為所有事物(除private
修飾)都可以多態地發生。然而現實卻不是這樣子的,只有普通的方法調用才可以是多態的。這邊是多態的誤區所在。
讓我們再看看下面這組代碼:
class Super { public int field = 0; public int getField() { return field; } }class Son extends Super { public int field = 1; public int getField() { return field; } public int getSuperField() { return super.field; } }class FieldTest { public static void main(String[] args) { Super sup = new Son(); System.out.println("sup.field:" + sup.field + " sup.getField():" + sup.getField()); Son son = new Son(); System.out.println("son.field:" + son.field + " son.getField:" + son.getField() + " son.getSupField:" + son.getSuperField()); } }/* OUTPUT sup.field:0 sup.getField():1 son.field:1 son.getField:1 son.getSupField:0 */復制代碼
從上面代碼中我們看到sup.field
輸出的值不是 Son
對象中所定義的,而是Super
本身定義的。這與我們認識的多態有點沖突。
其實不然,當Super
對象轉型為Son
引用時,任何域訪問操作都將由編譯器解析,因此不是多態的。在本例中,為Super.field
和Son.field
分配了不同的存儲空間,而Son
類是從Super
類導出的,因此,Son
實際上是包含兩個稱為field
的域:它自己的+Super
的。
雖然這種問題看上去很令人頭痛,但是我們開發規范中,通常會將所有的域都設置為 private,這樣就不能直接訪問它們,只能通過調用方法來訪問。
static 帶來的失靈:
看到這里,小伙伴們應該對多態有個大致的了解,但是不要掉以輕心哦,還有一種情況也是會出現失靈的,那就是如果某個方法是靜態的,那么它的行為就不具有多態性。
老規矩,我們看下這組代碼:
class StaticSuper { public static void staticTest() { System.out.println("StaticSuper staticTest()"); } }class StaticSon extends StaticSuper{ public static void staticTest() { System.out.println("StaticSon staticTest()"); } }class StaticTest { public static void main(String[] args) { StaticSuper sup = new StaticSon(); sup.staticTest(); } }/* OUTPUT StaticSuper staticTest() */復制代碼
靜態方法是與類相關聯,而非與對象相關聯
首先我們需要明白的是構造器不具有多態性,因為構造器實際上是static
方法,只不過該static
的聲明是隱式的。
我們先回到開頭的那段神秘代碼:
其中輸出結果是:
/* polygon() before cal() square.cal(), border = 0 polygon() after cal() square.square(), border = 4 */復制代碼
我們可以看到先輸出的是基類polygon
中構造器的方法。
這是因為基類的構造器總是在導出類的構造過程中被調用,而且是按照繼承層次逐漸向上鏈接,以使每個基類的構造器都能得到調用。
因為構造器有一項特殊的任務:檢查對象是否能正確的被構造。導出類只能訪問它自己的成員,不能訪問基類的成員(基類成員通常是private類型)。只有基類的構造器才具有權限來對自己的元素進行初始化。因此,必須令所有構造器都得到調用,否則就不可能正確構造完整對象。
步驟如下:
打個不是特別恰當的比方:你的出現是否先要有你父親,你父親的出現是否先要有你的爺爺,這就是逐漸向上鏈接的方式
有沒有想過如果在一個構造器的內調用正在構造的對象的某個動態綁定方法,那么會發生什么情況呢? 動態綁定的調用是在運行時才決定的,因為對象無法知道它是屬于方法所在的那個類還是那個類的導出類。如果要調用構造器內部的一個動態綁定方法,就要用到那個方法的被覆蓋后的定義。然而因為被覆蓋的方法在對象被完全構造之前就會被調用,這可能就會導致一些難于發現的隱藏錯誤。
問題引索:
一個動態綁定的方法調用會向外深入到繼承層次結構內部,它可以調動導出類里的方法,如果我們是在構造器內部這樣做,那么就可能會調用某個方法,而這個方法做操縱的成員可能還未進行初始化,這肯定就會招致災難的。
敏感的小伙伴是不是想到了開頭的那段代碼:
輸出結果是:
/* polygon() before cal() square.cal(), border = 0 polygon() after cal() square.square(), border = 4 */復制代碼
我們在進行square
對象初始化的時候,會先進行polygon
對象的初始化,在polygon
構造器中有個cal()
方法,這個時候就采用了動態綁定機制,調用了square
的cal()
,但這個時候border
這個變量尚未進行初始化,int 類型的默認值為 0,因此就有了square.cal(), border = 0
的輸出。看到這里,小伙伴們是不是有種撥開云霧見青天的感覺!
這組代碼初始化的實際過程為:
cal()
方法,由于步驟1的緣故,因此 border 的值為 0看完了這篇文章,相信你對java中多態的案例分析有了一定的了解,想了解更多相關知識,歡迎關注億速云行業資訊頻道,感謝各位的閱讀!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。