多態
多態的概念
多態的概念:通俗來說,就是多種形態,具體點就是去完成某個行為,當不同的對象去完成時會產生出不同的狀態。
多態實現條件?
在java中要實現多態,必須要滿足如下幾個條件,缺一不可:
1. 必須在繼承體系下
2. 子類必須要對父類中方法進行重寫
3. 通過父類的引用調用重寫的方法
多態體現:在代碼運行時,當傳遞不同類對象時,會調用對應類中的方法。
public class Animal { String name; int age;public Animal(String name, int age){ this.name = name; this.age = age; }public void eat(){ System.out.println(name + "吃飯"); }}public class Cat extends Animal{public Cat(String name, int age){ super(name, age); }@Override public void eat(){ System.out.println(name+"吃魚~~~"); }}public class Dog extends Animal {public Dog(String name, int age){ super(name, age); }@Override public void eat(){ System.out.println(name+"吃骨頭~~~"); }}///分割線//public class TestAnimal {// 編譯器在編譯代碼時,并不知道要調用Dog 還是 Cat 中eat的方法 // 等程序運行起來后,形參a引用的具體對象確定后,才知道調用那個方法 // 注意:此處的形參類型必須時父類類型才可以public static void eat(Animal a){ a.eat();}public static void main(String[] args) { Cat cat = new Cat("元寶",2); Dog dog = new Dog("小七", 1);eat(cat); eat(dog);}}運行結果: 元寶吃魚~~~ 元寶正在睡覺 小七吃骨頭~~~ 小七正在睡覺
在上述代碼中, 分割線上方的代碼是 類的實現者 編寫的, 分割線下方的代碼是 類的調用者 編寫的. 當類的調用者在編寫 eat 這個方法的時候, 參數類型為 Animal (父類), 此時在該方法內部并不知道, 也不關注當前的 a 引用指向的是哪個類型(哪個子類)的實例. 此時 a這個引用調用 eat方法可能會有多種不同的表現(和 a 引用的實例 相關), 這種行為就稱為 多態.
重寫
重寫(override):也稱為覆蓋。重寫是子類對父類非靜態、非private修飾,非?nal修飾,非構造方法等的實現過程 進行重新編寫, 返回值和形參都不能改變。即外殼不變,核心重寫!重寫的好處在于子類可以根據需要,定義特定 于自己的行為。 也就是說子類能夠根據需要實現父類的方法。
【方法重寫的規則】
子類在重寫父類的方法時,一般必須與父類方法原型一致: 返回值類型 方法名 (參數列表) 要完全一致
被重寫的方法返回值類型可以不同,但是必須是具有父子關系的
訪問權限不能比父類中被重寫的方法的訪問權限更低。例如:如果父類方法被public修飾,則子類中重寫該方 法就不能聲明為 protected
父類被static、private修飾的方法、構造方法都不能被重寫。
重寫的方法, 可以使用 @Override 注解來顯式指定. 有了這個注解能幫我們進行一些合法性校驗. 例如不小心 將方法名字拼寫錯了 (比如寫成 aet), 那么此時編譯器就會發現父類中沒有 aet 方法, 就會編譯報錯, 提示無法 構成重寫.
【重寫和重載的區別】
即:方法重載是一個類的多態性表現,而方法重寫是子類與父類的一種多態性表現。
?
【重寫的設計原則】
對于已經投入使用的類,盡量不要進行修改。最好的方式是:重新定義一個新的類,來重復利用其中共性的內容, 并且添加或者改動新的內容。 例如:若干年前的手機,只能打電話,發短信,來電顯示只能顯示號碼,而今天的手機在來電顯示的時候,不僅僅 可以顯示號碼,還可以顯示頭像,地區等。在這個過程當中,我們不應該在原來老的類上進行修改,因為原來的 類,可能還在有用戶使用,正確做法是:新建一個新手機的類,對來電顯示這個方法重寫就好了,這樣就達到了我 們當今的需求了。
?
靜態綁定:也稱為前期綁定(早綁定),即在編譯時,根據用戶所傳遞實參類型就確定了具體調用那個方法。典型代 表函數重載。
動態綁定:也稱為后期綁定(晚綁定),即在編譯時,不能確定方法的行為,需要等到程序運行時,才能夠確定具體 調用那個類的方法。
向上轉移和向下轉型
向上轉型
向上轉型:實際就是創建一個子類對象,將其當成父類對象來使用。
語法格式:父類類型 對象名 = new 子類類型()
Animal animal = new Cat("元寶",2);
animal是父類類型,但可以引用一個子類對象,因為是從小范圍向大范圍的轉換。
【使用場景】
1. 直接賦值
2. 方法傳參
3. 方法返回
public class TestAnimal {// 2. 方法傳參:形參為父類型引用,可以接收任意子類的對象
public static void eatFood(Animal a){ a.eat(); }// 3. 作返回值:返回任意子類對象 public static Animal buyAnimal(String var){ if("狗".equals(var) ){ return new Dog("狗狗",1); }else if("貓" .equals(var)){ return new Cat("貓貓", 1); }else{ return null; }}public static void main(String[] args) {Animal cat = new Cat("元寶",2); // 1. 直接賦值:子類對象賦值給父類對象 Dog dog = new Dog("小七", 1);eatFood(cat); eatFood(dog);Animal animal = buyAnimal("狗"); animal.eat();animal = buyAnimal("貓"); animal.eat();}}
向上轉型的優點:讓代碼實現更簡單靈活。
向上轉型的缺陷:不能調用到子類特有的方法。
向下轉型
將一個子類對象經過向上轉型之后當成父類方法使用,再無法調用子類的方法,但有時候可能需要調用子類特有的 方法,此時:將父類引用再還原為子類對象即可,即向下轉換。
public class TestAnimal {public static void main(String[] args) { Cat cat = new Cat("元寶",2); Dog dog = new Dog("小七", 1);// 向上轉型 Animal animal = cat; animal.eat(); animal = dog; animal.eat();// 編譯失敗,編譯時編譯器將animal當成Animal對象處理 // 而Animal類中沒有bark方法,因此編譯失敗 // animal.bark();// 向上轉型 // 程序可以通過編程,但運行時拋出異常---因為:animal實際指向的是狗 // 現在要強制還原為貓,無法正常還原,運行時拋出:ClassCastException cat = (Cat)animal; cat.mew();// animal本來指向的就是狗,因此將animal還原為狗也是安全的 dog = (Dog)animal; dog.bark();}}
向下轉型用的比較少,而且不安全,萬一轉換失敗,運行時就會拋異常。Java中為了提高向下轉型的安全性,引入了instanceof,如果該表達式為true,則可以安全轉換。
public class TestAnimal {public static void main(String[] args) { Cat cat = new Cat("元寶",2); Dog dog = new Dog("小七", 1);// 向上轉型 Animal animal = cat; animal.eat(); animal = dog; animal.eat();if(animal instanceof Cat){ cat = (Cat)animal; cat.mew(); }if(animal instanceof Dog){ dog = (Dog)animal; dog.bark(); }}}
?instanceof關鍵詞官方介紹:https://docs.oracle.com/javase/specs/jls/se8/html/jls-15.html#jls-15.20.2
多態的優缺點
假設有如下代碼:
class Shape { //屬性....public void draw() { System.out.println("畫圖形!"); } } class Rect extends Shape{ @Override public void draw() { System.out.println("?"); } } class Cycle extends Shape{ @Override public void draw() { System.out.println("●"); }} class Flower extends Shape{ @Override public void draw() { System.out.println("?"); } }
【使用多態的好處】
1. 能夠降低代碼的 "圈復雜度", 避免使用大量的 if - else
什么叫 "圈復雜度" ?
圈復雜度是一種描述一段代碼復雜程度的方式. 一段代碼如果平鋪直敘, 那么就比較簡單容易理解. 而如 果有很多的條件分支或者循環語句, 就認為理解起來更復雜.
因此我們可以簡單粗暴的計算一段代碼中條件語句和循環語句出現的個數, 這個個數就稱為 "圈復雜度". 如果一個方法的圈復雜度太高, 就需要考慮重構.
不同公司對于代碼的圈復雜度的規范不一樣. 一般不會超過 10 .
例如我們現在需要打印的不是一個形狀了, 而是多個形狀. 如果不基于多態, 實現代碼如下:
public static void drawShapes() {Rect rect = new Rect();Cycle cycle = new Cycle();Flower ?ower = new Flower();String[] shapes = {"cycle", "rect", "cycle", "rect", "?ower"};for (String shape : shapes) {if (shape.equals("cycle")) {cycle.draw();} else if (shape.equals("rect")) {rect.draw();} else if (shape.equals("?ower")) {?ower.draw();}}}
如果使用使用多態, 則不必寫這么多的 if - else 分支語句, 代碼更簡單.
public static void drawShapes() {// 我們創建了一個 Shape 對象的數組.Shape[] shapes = {new Cycle(), new Rect(), new Cycle(),new Rect(), new Flower()};for (Shape shape : shapes) {shape.draw(); }}
2. 可擴展能力更強
如果要新增一種新的形狀, 使用多態的方式代碼改動成本也比較低.
class Triangle extends Shape {@Overridepublic void draw() {System.out.println("△");}}
對于類的調用者來說(drawShapes方法), 只要創建一個新類的實例就可以了, 改動成本很低.
而對于不用多態的情況, 就要把 drawShapes 中的 if - else 進行一定的修改, 改動成本更高.
多態缺陷:代碼的運行效率降低。
1. 屬性沒有多態性
當父類和子類都有同名屬性的時候,通過父類引用,只能引用父類自己的成員屬性
2. 構造方法沒有多態性
見如下代碼~
避免在構造方法中調用重寫的方法
一段有坑的代碼. 我們創建兩個類, B 是父類, D 是子類. D 中重寫 func 方法. 并且在 B 的構造方法中調用 func
class B {public B() { // do nothing func(); }public void func() { System.out.println("B.func()"); }}class D extends B {private int num = 1; @Override public void func() { System.out.println("D.func() " + num); }}public class Test { public static void main(String[] args) {D d = new D();}}// 執行結果
D.func() 0
構造 D 對象的同時, 會調用 B 的構造方法.
B 的構造方法中調用了 func 方法, 此時會觸發動態綁定, 會調用到 D 中的 func
此時 D 對象自身還沒有構造, 此時 num 處在未初始化的狀態, 值為 0. 如果具備多態性,num的值應該是1.
所以在構造函數內,盡量避免使用實例方法,除了?nal和private方法。
結論: "用盡量簡單的方式使對象進入可工作狀態", 盡量不要在構造器中調用方法(如果這個方法被子類重寫, 就會觸 發動態綁定, 但是此時子類對象還沒構造完成), 可能會出現一些隱藏的但是又極難發現的問題.