1. 多态的基本介紹
多(多種)态(狀态)。 多态的概念:通俗來說,就是多種形态,具體點就是去完成某個行為,當不同的對象去完成時會産生出不同 的狀态。
舉個例子:動物有很多種類,狗、貓等等,在吃這個條件下;貓吃貓糧,狗吃狗糧。這就是多态的具體展現。
總之就是:同一件事情,發生在不同對象身上,就會産生不同的結果。
2. 多态實作條件
在java中要實作多态,必須要滿足如下幾個條件,缺一不可:
1. 必須在繼承體系下
2. 子類必須要對父類中方法進行重寫
3. 通過父類的引用調用重寫的方法
多态展現:在代碼運作時,當傳遞不同類對象時,會調用對應類中的方法。
案例:
public class demo {
public static void main(String[] args) {
// 編譯器在編譯代碼時,并不知道要調用Dog 還是 Cat 中eat的方法
// 等程式運作起來後,形參a引用的具體對象确定後,才知道調用那個方法
// 注意:此處的形參類型必須時父類類型才可以
Animal dog = new Dog();
dog.eat();
Animal cat = new Cat();
cat.eat();
}
}
class Animal {
public void eat() {
System.out.println("吃飯");
}
}
class Dog extends Animal {
public String name;
public void bark() {
System.out.println("汪汪汪!");
}
}
class Cat extends Animal {
public void miaow() {
System.out.println("喵喵喵!");
}
}
這個就是一個多态的向上轉型,Dog dog = new Dog() ; 這沒問題,而現在dog的類型為Animal了,類型不一樣;理論上來說類型不一樣的不可以這麼指派,為什麼這裡可以寫,那是因為二者構成了父子類關系。
那現在我們去試着調用子類中特有的方法:
編譯似乎不然通過,這是為什麼呢?
當我們的 " . " 之前的類型裡,是必須包含 " . " 之後的成員或方法,否則編譯不通過。當我們調用bark的時候,Animal中找不到Dog指向的bark,是以無法運作。
結論: 當我們發生向上轉型的時候,我們隻能調用父類有的成員和方法,不能調用子類特有的成員和方法。
向上轉型是多态發生的基礎。
還有其他向上轉型的其他例子:
public static void func(Animal animal){
}
public static void main(String[] args) {
Dog dog = new Dog();
func(dog);
}
public static Animal func(){
return new Dog();
}
public static void main(String[] args) {
Dog dog = new Dog();
func();
}
3. 重寫
我們上面貓和狗吃飯有點不太符合我們的實際需求,狗就吃狗糧,貓就吃貓糧,并且我不想在父類中對eat進行重寫。
我們将代碼進行修改:
public class demo {
public static void main(String[] args) {
Animal dog = new Dog();
dog.name = "小金毛";
dog.eat();
Animal cat = new Cat();
cat.name = "小折耳";
cat.eat();
}
}
class Animal {
public String name;
public int age;
public void eat() {
System.out.println("吃飯");
}
}
class Dog extends Animal {
public void bark() {
System.out.println("汪汪汪!");
}
@Override
public void eat() {
System.out.println(name + "吃狗糧");
}
}
class Cat extends Animal {
public void miaow() {
System.out.println("喵喵喵!");
}
@Override
public void eat() {
System.out.println(name + "吃貓糧");
}
}
注意看:
子類中有一個與父類同名的方法上面一行有一個 " @Override " 這個就是注解,注解有很多中,這裡的 " @Override " 是指對重寫的方法進行檢查,檢查你是否發生了重寫,如果沒有發生會報錯。
重寫的介紹:
重寫(override):也稱為覆寫。重寫是子類對父類非靜态、非private修飾,非final修飾,非構造方法等的實作過程
進行重新編寫, 傳回值和形參都不能改變。即外殼不變,核心重寫!重寫的好處在于子類可以根據需要,定義特定
于自己的行為。 也就是說子類能夠根據需要實作父類的方法。
【方法重寫的規則】
1. 子類在重寫父類的方法時,一般必須與父類方法原型一緻: 傳回值類型 方法名 (參數清單) 要完全一緻
2. 被重寫的方法傳回值類型可以不同,但是必須是具有父子關系的
3. 通路權限不能比父類中被重寫的方法的通路權限更低。例如:如果父類方法被public修飾,則子類中重寫該方法就不能聲明為 protected
4. 父類被static、private修飾的方法、構造方法都不能被重寫。
5. 重寫的方法, 可以使用 @Override 注解來顯式指定. 有了這個注解能幫我們進行一些合法性校驗. 例如不小心将方法名字拼寫錯了 (比如寫成 aet), 那麼此時編譯器就會發現父類中沒有 aet 方法, 就會編譯報錯, 提示無法構成重寫.
【重寫和重載的差別】
差別點 重寫(override) 重載(override)
參數清單 一定不能修改 必須修改
傳回類型 一定不能修改【除非可以構成父子類關系】 可以修改
通路限定符 一定不能做更嚴格的限制(可以降低限制) 可以修改
動、靜态綁定機制
至于重寫是怎麼發生的,這裡需要講一個動态綁定機制。
Java的動态綁定機制(非常重要):
1. 當調用對象方法時,該方法會和該對象的記憶體位址(運作類型)綁定
2. 當調用對象屬性時,沒有動态綁定機制,哪裡聲明,那裡使用。
動态綁定發生條件:
1. 向上轉型
2. 重寫
3. 通過父類引用調用父類和子類重寫的方法
靜态綁定:也稱為前期綁定(早綁定),即在編譯時,根據使用者所傳遞實參類型就确定了具體調用那個方法。典型代表:方法的重載。
5 向上轉型和向下轉型
向上轉型
向上轉型的本質:父類的引用指向了子類的對象。
文法:父類類型 對象名 = new 子類類型()
例如:
Animal animal = new Cat("元寶",2);
animal是父類類型,但可以引用一個子類對象,因為是從小範圍向大範圍的轉換。
貓屬于動物,但是動物不僅僅隻有貓。
【使用場景】
1. 直接指派
2. 方法傳參
3. 方法傳回
案例不做示範,和上面的案例差不多。
向上轉型的特點(總結):
1. 可以調用父類的所有成員,但是需要遵守權限。
2. 不能調用子類特有的成員、方法。//因為在編譯階段,能調用哪些成員是由編譯類型決定的
3. 最終運作效果看子類的具體表現,即調用時從子類開始查找方法,然後調用。
向上轉型的優點:讓代碼實作更簡單靈活。
向上轉型的缺陷:不能調用到子類特有的方法。
向下轉型
文法:子類類型 引用名 = (子類類型) 父類引用
例如: Cat cat = (Cat) animal;
要求:父類的引用必須指向的是目前目标類型的對象。
向上轉型是在堆上建立一個子類的對象把位址賦給了父類的引用,向下轉型就是這個父類的引用把開辟的子類的空間的位址回到了子類的引用,是以可以使用子類獨有的方法。
将一個子類對象經過向上轉型之後當成父類方法使用,再無法調用子類的方法,但有時候可能需要調用子類特有的方法,此時:将父類引用再還原為子類對象即可,即向下轉換。
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 關鍵詞官方介紹:Chapter 15. Expressions
多态的優缺點
假設有如下代碼:
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 flower = new Flower();
String[] shapes = {"cycle", "rect", "cycle", "rect", "flower"};
for (String shape : shapes) {
if (shape.equals("cycle")) {
cycle.draw();
} else if (shape.equals("rect")) {
rect.draw();
} else if (shape.equals("flower")) {
flower.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 {
@Override
public void draw() {
System.out.println("△");
}
}
對于類的調用者來說(drawShapes方法), 隻要建立一個新類的執行個體就可以了, 改動成本很低.
而對于不用多态的情況, 就要把 drawShapes 中的 if - else 進行一定的修改, 改動成本更高.
多态缺陷:代碼的運作效率降低。
1. 屬性沒有多态性
當父類和子類都有同名屬性的時候,通過父類引用,隻能引用父類自己的成員屬性
2. 構造方法沒有多态性