多型概述
多型性是繼封裝性和繼承性之后,面向物件的第三大特性,
多型性是面向物件編程的又一個重要特征,它是指在父類中定義的屬性和方法被子類繼承之后,可以具有不同的資料型別或表現出不同的行為,這使得同一個屬性或方法在父類及其各個子類中具有不同的含義,
對面向物件來說,多型分為編譯時多型和運行時多型,其中編譯時多型是靜態的,主要是指方法的多載,它是根據引數串列的不同來區分不同的方法,通過編譯之后會變成兩個不同的方法,在運行時談不上多型,而運行時多型是動態的,它是通過動態系結來實作的,也就是大家通常所說的多型性,
多型的體現
在Java中物件的多型性體現在父類參考指向子類的物件,
首先定義3個類,Person、Man、Woman,其中Person是Man和Woman的父類,
Person類:
public class Person {
String name;
int age;
public void eat(){
System.out.println("吃飯");
}
public void walk(){
System.out.println("走路");
}
}
Man類:
public class Man extends Person {
boolean isDrinking;
boolean isSmoking;
public void earnMoney(){
System.out.println("男人掙錢的方法");
}
@Override
public void eat() {
System.out.println("為了有力氣干活,我要吃2斤大肘子");
}
@Override
public void walk() {
System.out.print("男人邁著霸氣的步伐");
}
}
Woman類:
public class Woman extends Person{
boolean isBeauty;
public void goShopping() {
System.out.println("女人購物的方法");
}
@Override
public void eat() {
System.out.println("為了大漂亮,我只能吃二兩水煮大白菜");
}
@Override
public void walk() {
System.out.print("女人邁著婀娜的步伐");
}
}
多型之前實體化物件
參考變數是什么型別就new什么型別的物件
public void test1() {
// 參考變數型別為Person,new的也是Person
Person p1 = new Person();
Man man = new Man();
Woman man = new Woman();
}
使用多型實體化物件
父類參考指向子類物件(或子類物件賦值給父類參考)
// 格式:
父型別別 變數 = new 子類物件;
public void test2() {
// 參考為父類,new的物件為子類
Person p1 = new Man();
Person p2 = new Woman();
}
Java的參考變數有兩個型別,等號左邊型別稱為編譯時型別,等號右邊型別稱為運行時型別,
編譯時型別:宣告參考變數的型別,
運行時型別:實際賦給參考變數的型別,
當編譯時型別和運行時型別不一致時,就產生了物件的多型性,
向上轉型
如果是第一次接觸多型,可以會非常迷惑,為什么會存在這種定義的方式?
多型的定義方式,其實就是一種向上轉型的程序,對于如下的代碼大家肯定很容易理解:
short a = 10;
int b = a; // 自動型別提升
我們知道基本資料型別間可以發生自動型別轉換,比如將一個short型別的變數賦值給int型別,范圍小的型別會自動提升為范圍大的型別,如果對應到類的繼承關系中,顯然父類的適用范圍更加廣泛,而子類的適用范圍更加具體,因此我們可以將子類的向上轉型看作是基本型別的自動型別轉換,
至于為什么叫做向上轉型而不叫向下轉型,在繼承關系圖中一般是將父類作為上一個層級的存在,而子類作為下一個層級的存在,當子類需要型別轉換為父類時,一般都形象的稱為向上轉型,而向下轉型則表示子類的參考指向父類的物件,這個概念將在本文后續介紹,

大家肯定會有疑問,為什么子類的適用范圍要小于父類?明明子類繼承了父類所有的屬性和方法,并且還可以擁有自己特有的方法,這不是意味著子類用于的功能比父類更多嘛?
讓我們回到對于類的概念上,類是作為對一類具有相關屬性和行為的事物的描述,其中的屬性和行為越多對于事物的描述也就越具體,所以對比子類和父類,顯然對于子類的描述更加的具體,而對于父類的描述更加的抽象,父類所涵蓋的范圍也就更加廣泛,
向上轉型的三種時機
- 直接賦值
public void test2() {
// 向上轉型,子類物件賦值給父類參考
Person p1 = new Man();
Person p2 = new Woman();
Person p3;
Man m1 = new Man();
p3 = m1; // 父類參考指向了子類參考所指向的子類物件(表達有點繞,其實是一樣的)
}
- 方法的傳參
public void polymorphic(Person p1){
//...
}
public void test2() {
// 傳參中將子類物件作為實參傳遞給方法的父類參考形參,
polymorphic(new Man());
}
- 方法的回傳值
// 方法的回傳值為Person,將Person的子類物件作為回傳值傳遞
public Person polymorphic1(){
Man m = new Man();
return m;
}
public void test3(){
// 接收方法的回傳值也應該使用Person型別的參考變數接收
Person p1 = polymorphic1();
}
子類向上轉型后發生的改變
當子類向上轉型為父類后,其參考型別就是父型別別,通過父類的參考是無法訪問到子類物件中特有的屬性和方法,只能訪問父類中存在的屬性和方法,
// 試圖通過父類的參考訪問子類特有的屬性和方法時,編譯會報錯
public void test2() {
Person p1 = new Man();
Person p2 = new Woman();
p1.isDrinking = false;
p1.isSmoking = false;
p1.earnMoney();
p2.isBeauty = false;
p2.goShopping();
}



多型的使用
呼叫方法
如果大家可以接受向上轉型的機制,我們接下來繼續看看當通過多型進行呼叫方法時,會發生什么情況,
public void test3(){
Person p1 = new Man();
Person p2 = new Woman();
// 通過多型的方式呼叫父類存在的方法
p1.eat();
p1.walk();
p2.eat();
p2.walk();
}

從輸出結果這里看到,使用多型的方式呼叫父類中存在的方法時,實際上呼叫的是子類覆寫重寫后的方法,這里需要引入方法系結的概念,
系結是指將一個方法呼叫同一個方法主題關聯起來,Java中的系結分為靜態系結(前期系結)和動態系結(后期系結),
靜態系結,程式執行前方法已經被系結,此時由編譯器或其它連接程式實作,在Java語言中,private、static、final所修飾的方法以及構造器在編譯期間已經被系結,編譯器準確的知道應該呼叫哪個方法,這種呼叫方式被稱作靜態系結,
動態系結,程式執行前編譯器不知道物件的實際型別,在程式運行期間,JVM通過父類參考獲取其參考物件的實際型別,并定位到方法區中的該類的方法表,找到對應的方法進行呼叫,在Java中除了上述提到的幾種靜態系結方法以外,其它所有的方法都屬于動態系結,而動態系結是實作多型的關鍵,
Java在呼叫方法時,編譯期間會首先確定父類參考中是否存在所需要對應的方法,如果沒有則會向其父類中查找,直到找到Object類,如果Object類中仍然沒有該方法則會編譯報錯,
如果存在該方法時,編譯期間所呼叫的就是父類中存在的方法,在多型的前提下(即子類重寫了父類的方法時)父類被呼叫的方法也被稱作虛擬方法,

在運行期間,就發生了上面所介紹的動態系結,JVM會根據所呼叫的方法前的參考,找到實際的運行時型別并呼叫其中的所對應方法,如果沒有則向上查找,在本例中如果Man和Woman類中沒有覆寫重寫父類的方法,那么實際呼叫的還是父類的方法,不過這種情況完全沒有必要再使用多型的方式創建物件,
總結多型在呼叫方法時的特點就是:編譯看左,運行看右,
呼叫屬性
多型性并不適用于屬性,因為在繼承性中屬性是不會被重寫的,這也意味著子類中的重名屬性是屬于子類特有的屬性與父類無關,以多型的方式呼叫屬性時遵循編譯看左,運行也看左的規則,
- 編譯看左:在編譯期間會確定父類中是否存在對應屬性,沒有則編譯錯誤,
- 運行看左:運行期間父類參考所呼叫屬性也是父類中的屬性,即使子類中存在重名屬性也不會呼叫該屬性,
例如在子類中定義一個與父類同名的屬性:
當使用多型的方式呼叫name屬性時,實際上呼叫的時父類的name,

多型存在的必要條件
-
繼承
多型必須是在繼承的基礎之上才能產生的,如果兩個類不存在繼承關系,則更談不上下面所需的重寫和向上轉型,
-
重寫
如果子類沒有覆寫重寫父類的方法,那么多型的所要體現出的動態呼叫不同方法的意義也就不存在,因此也就沒有必要使用多型創建物件,
-
向上轉型
向上轉型也就是父類參考指向子類的物件,通過這種方式,可以使得一個父類(介面)接受不同的子類物件完成各自不同的功能,
多型的優點
使用多型可以提高我們開發的便利性和代碼的拓展性,如何理解這段話呢?
例如,需要定義一個功能,該功能實作了對餐廳對人類物件的服務,餐廳并不關心其所服務的物件是男人還是女人,只要是人該餐廳都能為其提供服務,
為了完成上述的功能,如果在沒有多型的前提下,我們需要分別為男人和女人同時定義一個功能相同的方法,
// 為男人定義該功能
public void Service(Man m){
m.walk();
System.out.println("走進餐廳");
System.out.println("請問您需要些什么?");
m.eat();
System.out.println("好的請您稍等");
}
// 為男人定義該功能
public void Service(Woman f){
f.walk();
System.out.println("走進餐廳");
System.out.println("請問您需要些什么?");
f.eat();
System.out.println("好的請您稍等");
}
如果使用多型來完成該項功能,我們可以只需要將接收物件的類定義為兩個類的父類就可以在一個方法中完成對不同物件的服務,
public void Service(Person p){
p.walk();
System.out.println("走進餐廳");
System.out.println("請問您需要些什么?");
p.eat();
System.out.println("好的請您稍等");
}

這里例子只存在兩個子類,如果是需要服務非常多的類,并且這些類都具有公共父類時,多型的好處就體現的非常明顯,可以使得我們極大的減少冗余的代碼,
同時在擴展性方法,不管今后添加多少子類,我們都可以使用這同一個方法來完成,
向下轉型
在前文中已經介紹了向上轉型的概念,而向下轉型的概念就是子類的參考指向父類的物件,
首先需要明確的是向下轉型屬于強制型別轉換,我們類比到基本資料型別的強制型別轉換,基本資料型別在強制型別轉換時可能會發生資料截斷,而作為對于類來說這個程序同樣是不安全的,因此在實際開發中需要謹慎使用,
向下轉型的格式
向下轉型同樣使用強制型別轉換符()來進行轉型,定義格式如下:
子型別別 變數名 = (子型別別) 父類變數名;
例如:使用Man的參考接收Person的物件,當程式運行時會發生什么?
Man m = (Man) new Person();

程式拋出了型別轉換例外ClassCastException,我們分別從概念和代碼層面上來討論為什么程式會報錯,
從概念上來說
一個男人可以是一個人(is a),但我們不能說一個人是一個男人,顯然人的表示范圍更加廣泛,因此從道德倫理上來說,這種行為也是反常態的,
從代碼上來說
子類中的屬性和方法一定是等于或者多于父類中的屬性和方法,那么我們如果我們將一個父類的物件成功賦值給子類的參考時,通過這個子類的參考去呼叫子類中特有的方法,父類的物件是不存在這些方法的,這顯然就會發生更加嚴重的錯誤,所以這種行為是不被允許的,
那如果我們將一個向上轉型的Woman類強轉為Man類時又會發生什么呢?
Person p = (Man) new Woman();
Man m = (Man) new Person();

程式也拋出了型別轉換例外,同樣從概念和代碼上進行討論
從概念上來說
讓一個女人變成一個男人顯然不是Java力所能及的范圍,
從代碼上來說
兩個繼承于同一個父類的不同子類他們的內部結構肯定是不相同的,如果允許這種行為的發生,同樣會導致無法預測的錯誤,
instanceof
上面的舉例說明了向下轉型中存在的隱患,那么向下轉型就沒有用了嘛?顯然也不能一概否定,當我們在對子類物件進行向上轉型后又需要向下轉型來呼叫子類特有的屬性和方法時,向下轉型的作用就體現出來了,但是鑒于上面可能發生的例外,我們應該在向下轉型之前使用instanceof運算子來判斷該物件是否為需要轉型后的物件,
使用格式
參考變數 instanceof 類A // 檢查該參考所指向的物件是否為類A的實體,如果是回傳true,否則回傳false
例如:定義一個方法,該方法使用了父類的參考接收物件,在其中需要呼叫子類所特有的屬性和方法,
public void instanceOfTest(Person p) {
if (p instanceof Man) {
Man m = (Man) p;
m.earnMoney();
m.isDrinking = false;
} else if (p instanceof Woman) {
Woman f = (Woman) p;
f.goShopping();
f.isBeauty = true;
}
}

對于向下轉型和instanceof操作我們應該盡量減少使用,因為這種轉型是無法被編譯器察覺出錯誤的,只有在程式運行期間才會拋出例外,當我們需要使用子類特有的方法時,應該首先檢查父類的設計是否合理,而不是直接使用向下轉型,
總結
關于多型性,屬于面向物件三大特性中最為抽象的一種特性,對于它的理解也更加困難,具體還需要落實到代碼層面多多體會,理解好了多型性對于抽象類和介面的理解也會更加深刻,
最后參考《Java編程思想》作者 Bruce Eckel 的一句話:不要犯傻,如果它不是晚系結(動態系結), 它就不是多型,
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/301999.html
標籤:其他
上一篇:?? 中秋佳節相約C語言進階 ?? 字串函式與記憶體函式 【建議收藏】
下一篇:你真的會給變數命名嗎
