1.多態的概念&作用
多態(Polymorphism)是面向對象編程的三大基本特性之一(封裝和繼承已經講過了),它允許不同類的對象對同一消息做出不同的響應。具體來說,多態允許基類/父類的引用指向派生類/子類的對象(向上轉型),并通過該引用調用子類中重寫的方法,從而實現不同的行為
2.實現多態的條件
在Java中,要實現多態必須滿足以下條件,缺一不可:
- 1.在繼承體系下
- 2.父類引用指向子類對象(向上轉型)
- 3.子類必須對父類中的可重寫方法進行重寫
- 4.通過父類引用調用子類中重寫的方法
在實現多態之前,我們必須先搞清楚向上轉型和重寫是什么
3.向上轉型
3.1 概念
向上轉型是面向對象編程中的一種類型轉換機制,它允許將子類的對象引用轉換為父類的引用。這種類型轉換是安全的,隱式的
3.2 語法格式
父類類型 變量名 = new 子類對象
public class Animal {public int age;public String name;
}
public class Dog extends Animal {public String color;
}
public class Test {public static void main(String[] args) {Animal dog = new Dog();}
}
在上述代碼中,Dog類繼承自Animal類,在實例化Dog對象的時候,創建了一個父類類型的引用(變量)來指向該對象,這就是向上轉型,下面我畫個圖來表示父類引用和子類對象在內存中的關系圖:
實現向上轉型有三種方式:
(1)直接賦值:
Animal dog = new Dog();
(2)方法傳參:
public class Test {public static void func(Animal a) {a.eat();}public static void main(String[] args) {Dog dog = new Dog();func(dog);}
}
(3)方法返回值:
public class Test {public static Animal func(String value) {if (value.equals("cat")) {return new Cat();}else if (value.equals("dog")) {return new Dog();}else {return null;}}public static void main(String[] args) {Animal animal = func("cat");}
}
注意:當發生向上轉型后,父類引用無法訪問子類中原有的屬性和方法,只能訪問從父類中繼承而來的屬性和方法,
也就是說父類引用的訪問范圍如下:
那么,在已經發生向上轉型后,如何才能訪問子類原有的屬性和方法呢?
有兩個辦法:
- 1.向下轉型
- 2.使用多態性
4.向下轉型
4.1 如何使用向下轉型
通過顯式地將父類引用轉換為子類引用,可以訪問子類原有的屬性和訪問
public class Animal {public int age;public String name;
}
public class Dog extends Animal {public String color;
}
public class Test {public static void main(String[] args) {//向上轉型Animal dog = new Dog();System.out.println(dog.age = 10);System.out.println(dog.name = "Dog");//向下轉型Dog dog1 = (Dog) dog;System.out.println(dog1.color = "白色");}
}
允許結果:
10
Dog
白色
4.2 向下轉型存在的風險
注意:
向下轉型在**編譯**時是允許的,但如果沒有正確地檢查對象的實際類型,運行時可能會拋出ClassCastException異常。這是因為父類引用可能實際上引用的是父類本身或其他子類的對象,而不是目標子類的對象
public class Animal {public int age;public String name;
}
//Dog類繼承Animal
public class Dog extends Animal {public String color;
}
//Cat類繼承Animal
public class Cat extends Animal {public int weight;
}
public class Test {public static void main(String[] args) {//向上轉型Animal dog = new Dog();System.out.println(dog.age = 10);System.out.println(dog.name = "Dog");//向下轉型Dog dog1 = (Dog) dog;System.out.println(dog1.color = "白色");//錯誤的向下轉型Cat cat = (Cat) dog;cat.weight = 10;}
}
上述代碼中,Dog和Cat都繼承自Animal,但是在發生向下轉型的時候,使用Cat類型的引用指向Dog對象。這在編譯時是不會報錯的,因為Cat和Dog同屬于Animal的子類,但是當程序運行之后就會拋出ClassCastException異常
因為向下轉型本質上是強制類型轉換,將Animal dog引用(指向Dog對象的引用)強轉為Dog類型是允許的,因為Dog類型的引用指向Dog對象很合理;但是,如果將Animal dog引用(指向Dog對象的引用)強轉為Cat類型,這是不允許的,因為Cat類型的引用無法指向Dog對象。這就相當于無法將boolean類型強轉為int類型
4.3 instanceof運算符
如果用戶錯誤地使用向下轉型,在編譯階段是不容易被察覺出來的,只有運行階段才會報錯。如果程序運行起來之后才發現錯誤,可能已經帶來了損失,所以為了規避這一情況,Java引入了instanceof運算符來幫助用戶檢測錯誤
public class Animal {public int age;public String name;
}
//Dog類繼承Animal
public class Dog extends Animal {public String color;
}
//Cat類繼承Animal
public class Cat extends Animal {public int weight;
}
public class Test {public static void main(String[] args) {//向上轉型Animal dog = new Dog();System.out.println(dog.age = 10);System.out.println(dog.name = "Dog");//向下轉型if (dog instanceof Dog) {//dog instanceof Dog 為true//說明該向下轉型是安全的Dog dog1 = (Dog) dog;}if (dog instanceof Cat) {//dog instanceof Cat 為false//說明該向下轉型是不安全的,不執行if語句中的代碼Cat cat = (Cat) dog;}}
}
5.重寫
5.1 概念
**重寫(Override)**是面向對象編程中的一個重要概念,它允許子類提供一個與父類中已定義的方法具有相同名稱、參數列表和返回類型的方法。重寫使得子類能夠改變或擴展父類方法的實現
5.2 語法格式
public class Animal {public int age;public String name;//父類的eat方法public Animal eat() {System.out.println("Animal is eating.");return null;}
}
public class Dog extends Animal {public String color;//重寫父類的eat方法@Overridepublic Dog eat() {System.out.println("Dog is eating.");return null;}
}
重寫的規則:
- 1.子類重寫父類的方法時,必須和父類的方法名、參數列表保持一致
- 2.返回值類型可以不一樣,但必須具有父子關系。上述代碼中,子類重寫的方法的返回值類型可以是被重寫方法返回值類型的子類
- 3.子類中重寫方法的訪問權限必須大于等于父類中被重寫的方法
- 4.父類中被static、final、private修飾的方法以及構造方法不能被重寫
- 5.重寫方法是可以借助
@Override
注解。雖然這個注解不影響方法的實現邏輯,但是可以幫助我們進行合法性檢查。比如:如果不小心將方法名寫錯了(寫成了ate),注解就會幫我們報錯- 6.重寫只針對方法,和屬性/變量無關
5.3 重寫和重載的區別
特性 | 重載(Overload) | 重寫(Override) |
---|---|---|
定義 | 在同一個類中,方法名相同但參數列表不同 | 在子類中重新定義一個與父類中已定義的方法具有相同簽名、返回值一樣或這具有父子關系的方法 |
目的 | 提供方法的不同實現,以適應不同的參數類型和數量 | 改變或擴展父類方法的實現 |
訪問限定修飾符 | 無要求 | 子類方法的訪問權限必須大于等于父類方法 |
調用時機 | 編譯時根據參數列表來確定調用哪個方法 | 運行時確定 |
關鍵字 | 無關鍵字 | 可以借助@Override注解 |
5.4 動態綁定
上面講向上轉型時講過,當父類引用指向子類對象時,該父類引用只能訪問子類中繼承自父類的屬性和方法。那么,當向上轉型和方法重寫同時發生時,會碰撞出怎么樣的火花呢?
public class Animal {public int age;public String name;//父類的eat方法public Animal eat() {System.out.println("Animal is eating.");return null;}
}
public class Dog extends Animal {public String color;//重寫父類的eat方法@Overridepublic Dog eat() {System.out.println("Dog is eating.");return null;}
}
public class Test {public static void main(String[] args) {Animal dog = new Dog();dog.eat();}
}
在上述代碼中,引用指向子類的對象,再通過該父類引用去調用父類中被重寫的eat方法,按照我們之前學習的知識,此時運行結果應該是:Animal is eating.
,但實際上:
運行結果:
Dog is eating.
這里可以得出一個結論:當父類引用去調用父類中被重寫的方法時,真正被調用的方法是子類中重寫的方法。
注意:
- 1.具體最終調用哪個方法,在編譯時無法確定,在運行時根據對象的實際類型來確定
在編譯時認為調用Animal中的eat方法,但是根據運行結果來看,實際上調用的是子類中重寫的eat方法 - 2.在運行時確定調用的具體方法,這稱之為動態綁定/后期綁定,也是實現多態的基礎
6. 多態
6.1 多態的具體實現
public class Animal {public int age;public String name;//父類的eat方法public Animal eat() {System.out.println("Animal is eating.");return null;}
}
public class Dog extends Animal {public String color;//重寫父類的eat方法@Overridepublic Dog eat() {System.out.println("Dog is eating.");return null;}
}
public class Cat extends Animal {public int weight;//重寫父類的eat方法@Overridepublic Cat eat() {System.out.println("Cat is eating.");return null;}
}
public class Test {public static void func(Animal animal) {animal.eat();}public static void main(String[] args) {Dog dog = new Dog();Cat cat = new Cat();func(dog);func(cat);}
}
運行結果:
Dog is eating.
Cat is eating.
這里我們再回顧一下多態的概念:多態允許基類/父類的引用指向派生類/子類的對象(向上轉型),并通過該引用調用子類中重寫的方法,從而實現不同的行為。
在上述func方法中,同樣是通過animal形參來調用eat方法,兩個運行的結果發生了變化,這個過程就叫做多態
6.2 使用多態降低圈復雜度
什么叫圈復雜度?
圈復雜度是一種描述一段代碼復雜程度的方式。如果一段代碼平鋪直敘,那么就比較容易理解;如果一段代碼使用很多的條件分支或者循環語句,就認為代碼理解起來比較復雜。
我們可以簡單地舉個例子,一層if-else語句表示一圈復雜度,如果一段代碼的圈復雜度太高,就需要考慮重新構建該代碼的結果。一般來說,圈復雜度不應該超過10。
現在我們需要打印多個圖形
public class Shape {public void draw() {System.out.println("Shape");}
}
public class Flower extends Shape {@Overridepublic void draw() {System.out.println("flower");}
}
public class Rect extends Shape{@Overridepublic void draw() {System.out.println("rect");}
}
public class Circle extends Shape {@Overridepublic void draw() {System.out.println("circle");}
}
下面是不使用多態的代碼:
public class Test {public static void main(String[] args) {String[] array = new String[]{"flower","rect","circle"};//for(String cur : array) {if(cur.equals("flower")) {new Flower().draw();}else if(cur.equals("rect")) {new Rect().draw();}else {new Circle().draw();}}}
}
下面是使用多態的代碼:
public class Test {public static void main(String[] args) {Shape[] array = new Shape[]{new Flower(), new Rect(), new Circle()};for(Shape cur : array) {cur.draw();}}
}
6.3 避免在構造方法中調用重寫的方法
實際執行結果:
Son0
期望執行結果:
Son10
上述代碼中,在父類的構造方法中調用func方法,此時調用的是子類中重寫的func方法。在子類的func方法中訪問了實例成員變量age,但是此時age還沒有開始初始化。因為此時仍然處于父類的構造方法中,而子類的實例成員變量初始化發生在父類構造方法結束之后。解決辦法有兩個:
- 1.將age變量使用static修飾(不建議)
- 2.避免在構造方法中調用重寫的方法(建議)