詳解Java多態對象的類型轉換與動態綁定_java

來源:互聯網
上載者:User

Java多態對象的類型轉換
這裡所說的物件類型轉換,是指存在繼承關係的對象,不是任意類型的對象。當對不存在繼承關係的對象進行強制類型轉換時,java 運行時將拋出 java.lang.ClassCastException 異常。

在繼承鏈中,我們將子類向父類轉換稱為“向上轉型”,將父類向子類轉換稱為“向下轉型”。

很多時候,我們會將變數定義為父類的類型,卻引用子類的對象,這個過程就是向上轉型。程式運行時通過動態綁定來實現對子類方法的調用,也就是多態性。

然而有些時候為了完成某些父類沒有的功能,我們需要將向上轉型後的子類對象再轉成子類,調用子類的方法,這就是向下轉型。

注意:不能直接將父類的對象強制轉換為子類類型,只能將向上轉型後的子類對象再次轉換為子類類型。也就是說,子類對象必須向上轉型後,才能再向下轉型。請看下面的代碼:

public class Demo {  public static void main(String args[]) {    SuperClass superObj = new SuperClass();    SonClass sonObj = new SonClass();    // 下面的代碼運行時會拋出異常,不能將父類對象直接轉換為子類類型    // SonClass sonObj2 = (SonClass)superObj;    // 先向上轉型,再向下轉型    superObj = sonObj;    SonClass sonObj1 = (SonClass)superObj;  }}class SuperClass{ }class SonClass extends SuperClass{ } 

將第7行的注釋去掉,運行時會拋出異常,但是編譯可以通過。

因為向下轉型存在風險,所以在接收到父類的一個引用時,請務必使用 instanceof 運算子來判斷該對象是否是你所要的子類,請看下面的代碼:

public class Demo {  public static void main(String args[]) {    SuperClass superObj = new SuperClass();    SonClass sonObj = new SonClass();    // superObj 不是 SonClass 類的執行個體    if(superObj instanceof SonClass){      SonClass sonObj1 = (SonClass)superObj;    }else{      System.out.println("①不能轉換");    }    superObj = sonObj;    // superObj 是 SonClass 類的執行個體    if(superObj instanceof SonClass){      SonClass sonObj2 = (SonClass)superObj;    }else{      System.out.println("②不能轉換");    }  }}class SuperClass{ }class SonClass extends SuperClass{ }

運行結果:

①不能轉換

總結:對象的類型轉換在程式運行時檢查,向上轉型會自動進行,向下轉型的對象必須是當前參考型別的子類。

Java多態和動態綁定
在Java中,父類的變數可以引用父類的執行個體,也可以引用子類的執行個體。

請讀者先看一段代碼:

public class Demo {  public static void main(String[] args){    Animal obj = new Animal();    obj.cry();    obj = new Cat();    obj.cry();    obj = new Dog();    obj.cry();  }}class Animal{  // 動物的叫聲  public void cry(){    System.out.println("不知道怎麼叫");  }  }class Cat extends Animal{  // 貓的叫聲  public void cry(){    System.out.println("喵喵~");  }}class Dog extends Animal{  // 狗的叫聲  public void cry(){    System.out.println("汪汪~");  }}

運行結果:

不知道怎麼叫喵喵~汪汪~

上面的代碼,定義了三個類,分別是 Animal、Cat 和 Dog,Cat 和 Dog 類都繼承自 Animal 類。obj 變數的類型為 Animal,它既可以指向 Animal 類的執行個體,也可以指向 Cat 和 Dog 類的執行個體,這是正確的。也就是說,父類的變數可以引用父類的執行個體,也可以引用子類的執行個體。注意反過來是錯誤的,因為所有的貓都是動物,但不是所有的動物都是貓。

可以看出,obj 既可以是人類,也可以是貓、狗,它有不同的表現形式,這就被稱為多態。多態是指一個事物有不同的表現形式或形態。

再比如“人類”,也有很多不同的表達或實現,TA 可以是司機、教師、醫生等,你憎恨自己的時候會說“下輩子重新做人”,那麼你下輩子成為司機、教師、醫生都可以,我們就說“人類”具備了多態性。

多態存在的三個必要條件:要有繼承、要有重寫、父類變數引用子類對象。

當使用多態方式調用方法時:
首先檢查父類中是否有該方法,如果沒有,則編譯錯誤;如果有,則檢查子類是否覆蓋了該方法。
如果子類覆蓋了該方法,就調用子類的方法,否則調用父類方法。

從上面的例子可以看出,多態的一個好處是:當子類比較多時,也不需要定義多個變數,可以只定義一個父類類型的變數來引用不同子類的執行個體。請再看下面的一個例子:

public class Demo {  public static void main(String[] args){    // 藉助多態,主人可以給很多動物餵食    Master ma = new Master();    ma.feed(new Animal(), new Food());    ma.feed(new Cat(), new Fish());    ma.feed(new Dog(), new Bone());  }}// Animal類及其子類class Animal{  public void eat(Food f){    System.out.println("我是一個小動物,正在吃" + f.getFood());  }}class Cat extends Animal{  public void eat(Food f){    System.out.println("我是一隻小貓咪,正在吃" + f.getFood());  }}class Dog extends Animal{  public void eat(Food f){    System.out.println("我是一隻狗狗,正在吃" + f.getFood());  }}// Food及其子類class Food{  public String getFood(){    return "事物";  }}class Fish extends Food{  public String getFood(){    return "魚";  }}class Bone extends Food{  public String getFood(){    return "骨頭";  }}// Master類class Master{  public void feed(Animal an, Food f){    an.eat(f);  }}

運行結果:

我是一個小動物,正在吃事物我是一隻小貓咪,正在吃魚我是一隻狗狗,正在吃骨頭

Master 類的 feed 方法有兩個參數,分別是 Animal 類型和 Food 類型,因為是父類,所以可以將子類的執行個體傳遞給它,這樣 Master 類就不需要多個方法來給不同的動物餵食。
動態綁定

為了理解多態的本質,下面講一下Java調用方法的詳細流程。

1) 編譯器查看對象的宣告類型和方法名。

假設調用 obj.func(param),obj 為 Cat 類的對象。需要注意的是,有可能存在多個名字為func但參數簽名不一樣的方法。例如,可能存在方法 func(int) 和 func(String)。編譯器將會一一列舉所有 Cat 類中名為func的方法和其父類 Animal 中訪問屬性為 public 且名為func的方法。

這樣,編譯器就獲得了所有可能被調用的候選方法列表。

2) 接下來,編澤器將檢查調用方法時提供的參數簽名。

如果在所有名為func的方法中存在一個與提供的參數簽名完全符合的方法,那麼就選擇這個方法。這個過程被稱為重載解析(overloading resolution)。例如,如果調用 func("hello"),編譯器會選擇 func(String),而不是 func(int)。由於自動類型轉換的存在,例如 int 可以轉換為 double,如果沒有找到與調用方法參數簽名相同的方法,就進行類型轉換後再繼續尋找,如果最終沒有匹配的類型或者有多個方法與之匹配,那麼編譯錯誤。

這樣,編譯器就獲得了需要調用的方法名字和參數簽名。

3) 如果方法的修飾符是private、static、final(static和final將在後續講解),或者是構造方法,那麼編譯器將可以準確地知道應該調用哪個方法,我們將這種調用方式 稱為靜態繫結(static binding)。

與此對應的是,調用的方法依賴於對象的實際類型, 並在運行時實現動態綁。例如調用 func("hello"),編澤器將採用動態綁定的方式產生一條調用 func(String) 的指令。

4)當程式運行,並且釆用動態綁定調用方法時,JVM一定會調用與 obj 所引用對象的實際類型最合適的那個類的方法。我們已經假設 obj 的實際類型是 Cat,它是 Animal 的子類,如果 Cat 中定義了 func(String),就調用它,否則將在 Animal 類及其父類中尋找。

每次調用方法都要進行搜尋,時間開銷相當大,因此,JVM預先為每個類建立了一個方法表(method lable),其中列出了所有方法的名稱、參數簽名和所屬的類。這樣一來,在真正調用方法的時候,虛擬機器僅尋找這個表就行了。在上面的例子中,JVM 搜尋 Cat 類的方法表,以便尋找與調用 func("hello") 相匹配的方法。這個方法既有可能是 Cat.func(String),也有可能是 Animal.func(String)。注意,如果調用super.func("hello"),編譯器將對父類的方法表迸行搜尋。

假設 Animal 類包含cry()、getName()、getAge() 三個方法,那麼它的方法表如下:
cry() -> Animal.cry()
getName() -> Animal.getName()
getAge() -> Animal.getAge()

實際上,Animal 也有預設的父類 Object(後續會講解),會繼承 Object 的方法,所以上面列舉的方法並不完整。

假設 Cat 類覆蓋了 Animal 類中的 cry() 方法,並且新增了一個方法 climbTree(),那麼它的參數列表為:
cry() -> Cat.cry()
getName() -> Animal.getName()
getAge() -> Animal.getAge()
climbTree() -> Cat.climbTree()

在啟動並執行時候,調用 obj.cry() 方法的過程如下:
JVM 首先訪問 obj 的實際類型的方法表,可能是 Animal 類的方法表,也可能是 Cat 類及其子類的方法表。
JVM 在方法表中搜尋與 cry() 匹配的方法,找到後,就知道它屬於哪個類了。
JVM 調用該方法。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.