# 多態
[TOC]
## 導學
本章節,我們將要學習面向對象三大特性之一的多態,所謂的多態,字面意義來看就是多種形態。它是面向對象程序設置的最核心的特征。從某種意義上來講,封裝和繼承都是為了多態而準備的。
## 多態的概念
在現實生活中,動物都有吃東西,跑和跳等通用的行為能力,但是不同的動物針對行為的表現形式是不同的。比如貓,狗,兔子喜歡吃的東西各有不同,而叫聲也是不一樣的。

再比如鍵盤上的`f1`鍵,在eclipse的界面會喚出eclipse的幫助文檔,在word的界面會喚出word的幫助文檔,在Windows系統下回喚出Windows的幫助文檔。可以看到同樣的行為在不同的對象上會產生不同的形式結果,這就是生活中的多態。
在程序中:
>[info]多態意味著允許不同類的對象對同一消息作出不同的響應。
在Java中,多態在廣義上來說可以分為編譯時多態(方法的多態性)和運行時多態(對象的多態性)。
~~~
1. 編譯時多態(也叫設計時多態,通過方法重載實現)
2. 運行時多態(程序運行時動態決定調用哪個方法)
~~~
在Java中指的多態,大多指的是運行時多態(狹義上的多態)。
多態實現的具體條件在于:
1. 滿足繼承關系
2. 父類引用指向子類對象
那么,什么是父類引用指向子類對象呢?接下來通過具體代碼來看看吧
## 多態的實現
### 場景描述及實體類編寫

~~~
package com.dodoke.proJ.animal;
public class Animal {
//屬性:昵稱、年齡
private String name;
private int month;
//方法:吃東西
public void eat() {
System.out.println("動物都有吃東西的能力");
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getMonth() {
return month;
}
public void setMonth(int month) {
this.month = month;
}
public Animal(String name, int month) {
super();
this.name = name;
this.month = month;
}
public Animal() {
super();
}
}
~~~
~~~
package com.dodoke.proJ.animal;
public class Cat extends Animal {
//屬性:體重
private double weight;
//方法:跑動
public void run() {
System.out.println("小貓快樂的奔跑");
}
//方法:吃東西
@Override
public void eat() {
System.out.println("貓吃魚~");
}
public double getWeight() {
return weight;
}
public void setWeight(double weight) {
this.weight = weight;
}
public Cat() {
super();
}
public Cat(String name, int month, double weight) {
super(name, month);
this.weight = weight;
}
}
~~~
~~~
package com.dodoke.proJ.animal;
public class Dog extends Animal{
//屬性:性別
private String sex;
//方法:睡覺
public void sleep() {
System.out.println("小狗有午睡的習慣");
}
//方法:吃東西
@Override
public void eat() {
System.out.println("狗吃肉~");
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public Dog() {
super();
}
public Dog(String name, int month, String sex) {
this.setMonth(month);
this.setMonth(month);
this.sex = sex;
}
}
~~~
**多態是指編譯時類型和運行時類型不一致**
>[warning] Java引用類型變量有兩種類型:一種是編譯時類型,一種是運行時類型
* 編譯的類型是由聲明變量的時候確定的
* 運行時的類型是由實際上賦給該變量的對象決定的
~~~java
//比如聲明變量類型為Animal,實際賦給該變量的對象類型為Dog
Animal animal = new Dog();
~~~
### 向上轉型
**向上轉型是指父類引用指向子類的實例**
通常也稱向上轉型為隱式轉型和自動轉型。通俗來說,是子類轉型為父類,小類轉型為大類。
~~~
public class Test {
public static void main(String[] args) {
//animal的引用 = 具體的animal實例
Animal one = new Animal();
Animal two = new Cat();
Animal three = new Dog();
one.eat();
two.eat();
three.eat();
one.getMonth();
two.getName();
//three.sleep();
}
}
~~~
>[success]父類引用指向子類實例,可以調用子類重寫父類的方法以及父類派生的方法,但是無法調用子類獨有方法!
>[warning]父類中的靜態方法無法被重寫,所以向上轉型后,只能調用到父類原有的靜態方法
### 向下轉型
**向下轉型指的是子類引用指向父類對象**
向下轉型又稱之為強制類型轉換,通俗來講就是父類轉型為子類,大類轉為小類。對比之前的基本數據類型轉換,數據范圍大的類型轉為數據范圍小的類型就需要進行強制類型轉換,引用數據類型也是如此。
~~~
public class Test {
public static void main(String[] args) {
//animal的引用 = 具體的animal實例
Animal one = new Animal();
Animal two = new Cat();
Animal three = new Dog();
one.eat();
two.eat();
three.eat();
one.getMonth();
two.getName();
//three.sleep();
System.out.println("=======================================");
Cat temp = (Cat)two;
temp.eat();
temp.run();
temp.getMonth();
/**
* 此處的代碼不能進行強制類型轉換
* 原因:two這個對象定義的時候,實際上指向的是Cat這個類型的空間
* Cat temp = (Cat)two; 相當于把two對象還原成原來的實例空間。
* 但是狗和貓并沒有兼容的關系,只是擁有同一個父類而已,是兄弟關系
*/
Dog temp1 = (Dog)two;//必須滿足轉型條件才能轉換
temp1.eat();
temp1.sleep();
temp1.getMonth();
System.out.println("程序繼續執行");
}
}
~~~
**比較:**
| 向上轉型 | 向下轉型 |
| --- | --- |
| 又稱隱式轉型、自動轉型 | 又稱強制類型轉換 |
| 父類引用指向子類實例,可以調用子類重寫父類的方法以及父類派生的方法,無法調用子類獨有方法 | 子類引用指向父類對象,此處必須進行強制轉換,可以調用子類特有的方法 |
| 小類轉為大類 | 必須滿足轉型條件才能進行轉換 |
### instanceof關鍵字
在之前的章節中,我們提到向下轉型需要滿足轉型條件,那么我們該如何才能知道是否滿足轉型條件呢
這時候,`instanceof`關鍵字就派上用處了!

`instanceof`關鍵字用于判斷左邊的對象是否滿足右邊的實例。如果滿足返回`true`,否則返回`false`。
~~~
public class Test {
public static void main(String[] args) {
//animal的引用 = 具體的animal實例
Animal one = new Animal();
Animal two = new Cat();
Animal three = new Dog();
one.eat();
two.eat();
three.eat();
one.getMonth();
two.getName();
//three.sleep();
System.out.println("=======================================");
if(two instanceof Cat) {
Cat temp = (Cat)two;
temp.eat();
temp.run();
temp.getMonth();
System.out.println("two可以轉換為Cat類型");
}
if(two instanceof Dog) {
Dog temp1 = (Dog)two;
temp1.eat();
temp1.sleep();
temp1.getMonth();
System.out.println("two可以轉換為Dog類型");
}
if(two instanceof Animal) {
System.out.println("Animal");
}
if(two instanceof Object) {
System.out.println("Object");
}
//所以two對象具有Animal類型和Object類型的特征
System.out.println("程序繼續執行");
}
}
~~~
### 類型轉換案例
#### 案例
在原有的項目中,增加一個主人類。在貓類中添加玩線球方法
~~~
public class Master {
/**
* 喂寵物:
* 喂貓咪:吃完東西后,主人會帶著去玩線球
* 喂狗狗:吃完東西后,主人會帶著去睡覺
* 養兔子、樣鸚鵡、養烏龜
*/
//方案一:編寫方法,傳入不同類型的動物,調用各自的方法
public void feed(Cat cat) {
cat.eat();
cat.palyBall();
}
public void feed(Dog dog) {
dog.eat();
dog.sleep();
}
//方案二:編寫方法傳入動物的父類,方法中通過類型轉換,調用指定的子類的方法
public void feed(Animal ani) {
if(ani instanceof Cat) {
Cat cat = (Cat)ani;
cat.eat();
cat.palyBall();
} else if(ani instanceof Dog) {
Dog dog = (Dog)ani;
dog.eat();
dog.sleep();
}
}
}
~~~
多個對象要實現一個方法吃的時候,一般的解決方法是寫重載函數。
而新的解決方法是用一個方法傳參的時候先向上轉化成父類, 然后再根據實際情況進行判斷后,轉化成原來的類型 ,在調用自己本身的方法 (向下轉型)
#### 新增需求
針對主人空閑時間的判斷,如果時間多則養狗,如果時間少則養貓。
~~~
package com.dodoke.proJ.animal;
public class Master {
//方案一
public Dog hasManyTime() {
System.out.println("主人空閑時間充足,適合養狗狗");
return new Dog();
}
public Cat hasLittleTime() {
System.out.println("主人空閑時間比較少,適合養貓咪");
return new Cat();
}
//方案二
public Animal raise(boolean isManyTime) {
if(isManyTime) {
System.out.println("主人空閑時間充足,適合養狗狗");
return new Dog();
} else {
System.out.println("主人空閑時間比較少,適合養貓咪");
return new Cat();
}
}
}
~~~
在方法內部實現多態。返回值為子類對象,由父類引用接收。相當于Animal animal=new Cat();向上轉型。
當我們在實際開發當中需要同一個操作行為,針對不同的參數, 返回不同的實例對象,完成不同的操作結果的時候,就比較適用于這種多態的操作。類型轉換的優勢就會充分體現出來了。
## 抽象類
在之前的代碼中,我們創建了一個動物類。接下來,我們來看看這段代碼
~~~
Animal pet = new Animal("花花",2);
pet.eat();
~~~
針對于這樣的代碼,編譯運行都沒有什么問題,但是它在實際的開發中并沒有有效的意義。每個動物都應該有具體的吃東西的行為,實例化pet對象沒有意義。實際的開發中會指代具體的貓還是狗來完成指代。如上的代碼并不符合程序的邏輯。
那么,該如何限制程序員寫這些沒有意義的代碼呢?
我們可以使用`abstract`關鍵字來限制類的實例化。
>[danger]`abstract` 是不能修飾成員變量的;
### 抽象類
~~~
public abstract class Animal {}
abstract public class Animal {}
~~~
抽象數據類型: 在類的定義前加上 `abstract` 關鍵字,使得該類成為一個抽象類
**抽象類不允許被實例化**,但是可以通過向上轉型,指向子類實例,調用子類重寫父類的方法以及父類派生的方法
`public` 與 `abstract `可以互換,但是卻不能 與class關鍵字相互換位。
抽象類利用子類與父類的繼承關系,既限制了子類的設計隨意性,又避免了父類無意義的實例化。
>[info]應用場景:某個父類只是知道其子類應該包含怎樣的方法,但無法準確知道這些子類如何實現這些方法
### 抽象方法
~~~
public abstract class Animal {
//屬性:昵稱、年齡
private String name;
private int month;
/**
* 抽象方法:沒有方法體,在子類中必須重寫抽象方法,如果子類不重寫,則子類只能設置為抽象類
*/
public abstract void eat();
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getMonth() {
return month;
}
public void setMonth(int month) {
this.month = month;
}
public Animal(String name, int month) {
super();
this.name = name;
this.month = month;
}
public Animal() {
super();
}
}
~~~
含有抽象方法的類,只能被定義為抽象類。抽象類中不一定含有抽象方法。
在抽象類中的成員方法可以包括一般方法和抽象方法。
抽象類不能實例化,即使抽象類中不包含抽象方法,這個抽象類也不能創建實例。
一個類繼承抽象類后,必須實現其所有的抽象方法,否則也是抽象類,不同的子類對父類的抽象方法可以有不同的實現。
>[warning]如果方法定義為 static,就不能使用 abstract 修飾符;
如果方法定義為 private ,也不能使用 abstract 修飾符;
抽象類體現的是一種模板模式的設計思想,抽象類作為多個子類的通用模板,子類在抽象的基礎上進行擴充,但是子類整體上會保留抽象類的行為方法(必須要實現抽象類的抽象方法)。
抽象類一般只是定義需要使用的方法,把不能實現的部分抽象成抽象方法,留給子類去實現。
父類中可以有實現的方法,但是子類也是可以對已經實現的方法進行改造的(override),但是如果在子類中還需要調用父類的實現方法,可以使用 super 關鍵字。
## 練習
一、選擇
1. 下列代碼的運行結果為:

~~~
A. 我是動物
B. 編譯錯誤
C. 我是動物
我是老虎
我是哈士奇
D. 我是動物 我是老虎 我是哈士奇
~~~
2. 創建一個父類Animal,一個子類Cat,Animal three = new Cat();不是
~~~
A. 向上轉型
B. 自動轉型
C. 向下轉型
D. 隱式轉型
~~~
3. 下列代碼怎么修改可以使其成功運行:

~~~
A. 刪除掉標注3位置的one.fly( )
B. 標注1的Animal one=new Bird()修改為Animal one=new Animal()
C. 刪除掉標注2位置的one.eat( )
D. 標注1的Animal one=new Bird()修改為Bird one=new Animal()
~~~
4. 下列關于instanceof說法不正確的是
~~~
A. instanceof 的返回值為true和false
B. instanceof可以用來判斷對象是否可滿足某個特定類型
C. 可以通過“A instanceof B"表示 A 類可以轉型為B類
D. instanceof可放在if語句的條件表達式中
~~~
5. 已知父類Person,子類Man。判斷類Person的對象person1是否滿足類Man的實例特征,正確的語句為

~~~
A. if (person1? instanceof? Man)
B. if (man1? instanceof? Person)
C. if (Person? instanceof? man1)
D. if (Man? instanceof? person1)
~~~
6. 在Java中,多態的實現不僅能減少編碼的工作量,還能大大提高程序的可維護性及可擴展性,那么實現多態的步驟包括以下幾個方面除了
~~~
A. 子類重寫父類的方法
B. 子類方法設置為final類型
C. 定義方法時,把父類類型作為參數類型;調用方法時,把父類或子類的對象作為參數傳入方法
D. 運行時,根據實際創建的對象類型動態決定使用哪個方法
~~~
7. 下面代碼運行測試后,出現的結果是

~~~
A. 編譯錯誤,錯誤位置在第一行
B. 編譯錯誤,錯誤位置在第二行
C. 第一行和第二行都運行成功,輸出結果為
兒子
女兒
D. 編譯成功,但運行報錯,錯誤位置在第二行
~~~
8. 下面代碼怎么修改可以編譯時不報錯(多選)

~~~
A. 在位置一處將SpeedBike類設為抽象類,同時將位置2處的speedup也設為抽象方法
B. 將位置一中的public改為final
C. 將位置二中speedup()方法改為抽象方法
D. 將位置二中speedup()方法中加入方法的實現
~~~
9. 下列選項中,關于Java的抽象類和抽象方法說法不正確的是
~~~
A. 抽象類和抽象方法都通過abstract關鍵字來修飾
B. 抽象類中必須包含抽象方法
C. 抽象方法只有方法聲明,沒有方法實現
D. 子類如果不重寫父類所有的抽象方法,則必須設置為抽象類
~~~
二、編程
1. 應用繼承和多態的思想,編寫動物類,成員方法是動物叫聲。寫三個具體的類(貓、狗、羊),它們都是動物類的子類,并重寫父類的成員方法。編寫測試類,隨機產生三種具體動物,調用叫聲這個方法。
程序參考運行效果如圖所示:

**任務分析:**
**1.??? ? 定義一個父類Animal類**
屬性:kind(種類)
方法:創建帶參(kind為參數)構造方法
? ? ? ? ? ? 創建cry():void方法
**2.??? ? 編寫三個具體的子類Cat類、Dog類、Sheep類**
分別重寫父類中的 cry() 方法,輸出信息分別為
Cat類:小貓的叫聲:喵喵喵~~~
Dog類:小狗的叫聲:汪汪汪~~~
Sheep類:小羊的叫聲:咩咩咩~~~
**3.??? ? 編寫測試類,首先生成長度為5的父類對象數組,然后通過循環依次向數組中存入數據,現設定存儲規則為:**
a)??? ? 每次隨機產生一個0~2的正整數
b)?? ? 若數值為 0,則生成一個 Cat 類的對象,存入數組
c)??? ? 若數值為 1,則生成一個 Dog 類的對象,存入數組
d)?? ? 若數值為 2,則生成一個 Sheep 類的對象,存入數組
最后循環輸出數組成員,并分別調用 cry() 方法。