Java多態對象的類型轉換
這裏所說的對象類型轉換,是指存在繼承關係的對象,不是任意類型的對象。當對不存在繼承關係的對象進行強制類型轉換時,java 運行時將拋出 java.lang.ClassCastException 異常。java
在繼承鏈中,咱們將子類向父類轉換稱爲「向上轉型」,將父類向子類轉換稱爲「向下轉型」。spa
不少時候,咱們會將變量定義爲父類的類型,卻引用子類的對象,這個過程就是向上轉型。程序運行時經過動態綁定來實現對子類方法的調用,也就是多態性。.net
然而有些時候爲了完成某些父類沒有的功能,咱們須要將向上轉型後的子類對象再轉成子類,調用子類的方法,這就是向下轉型。code
注意:不能直接將父類的對象強制轉換爲子類類型,只能將向上轉型後的子類對象再次轉換爲子類類型。也就是說,子類對象必須向上轉型後,才能再向下轉型。請看下面的代碼:htm
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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 運算符來判斷該對象是不是你所要的子類,請看下面的代碼:繼承
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
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{ }
|
運行結果:ci
1
|
①不能轉換
|
總結:對象的類型轉換在程序運行時檢查,向上轉型會自動進行,向下轉型的對象必須是當前引用類型的子類。get
Java多態和動態綁定
在Java中,父類的變量能夠引用父類的實例,也能夠引用子類的實例。編譯器
請讀者先看一段代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
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(
"汪汪~"
);
}
}
|
運行結果:
1
2
3
|
不知道怎麼叫
喵喵~
汪汪~
|
上面的代碼,定義了三個類,分別是 Animal、Cat 和 Dog,Cat 和 Dog 類都繼承自 Animal 類。obj 變量的類型爲 Animal,它既能夠指向 Animal 類的實例,也能夠指向 Cat 和 Dog 類的實例,這是正確的。也就是說,父類的變量能夠引用父類的實例,也能夠引用子類的實例。注意反過來是錯誤的,由於全部的貓都是動物,但不是全部的動物都是貓。
能夠看出,obj 既能夠是人類,也能夠是貓、狗,它有不一樣的表現形式,這就被稱爲多態。多態是指一個事物有不一樣的表現形式或形態。
再好比「人類」,也有不少不一樣的表達或實現,TA 能夠是司機、教師、醫生等,你憎恨本身的時候會說「下輩子從新作人」,那麼你下輩子成爲司機、教師、醫生均可以,咱們就說「人類」具有了多態性。
多態存在的三個必要條件:要有繼承、要有重寫、父類變量引用子類對象。
當使用多態方式調用方法時:
首先檢查父類中是否有該方法,若是沒有,則編譯錯誤;若是有,則檢查子類是否覆蓋了該方法。
若是子類覆蓋了該方法,就調用子類的方法,不然調用父類方法。
從上面的例子能夠看出,多態的一個好處是:當子類比較多時,也不須要定義多個變量,能夠只定義一個父類類型的變量來引用不一樣子類的實例。請再看下面的一個例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
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);
}
}
|
運行結果:
1
2
3
|
我是一個小動物,正在吃事物
我是一隻小貓咪,正在吃魚
我是一隻狗狗,正在吃骨頭
|
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 調用該方法。