首頁  >  文章  >  Java  >  詳解Java多型物件的型別轉換與動態綁定

詳解Java多型物件的型別轉換與動態綁定

高洛峰
高洛峰原創
2017-01-19 13:45:261334瀏覽

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 呼叫該方法。

更多詳解Java多態性物件的型別轉換與動態綁定相關文章請關注PHP中文網!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn