Java子類與父類之間的物件轉換(說明繼承)
在使用Java的多型機制時,常常使用的一個特性便是子類和父類之間的物件轉換。從子類向父類的轉換稱為向上轉換(upcasting),通過向上轉換,我們能夠在編寫程式時採用通用程式設計的思想,在需要使用子類物件的時候,通過把變數定義為父型別,我們可以通過一個變數,使用該父型別的所有子型別例項;從父型別向子型別的轉換稱為向下轉換(downcasting),通過向下轉換,我們能在必要的時候,將父型別變數轉換成子型別變數,使用一些通過子型別才能夠使用的方法。以下是我對於物件轉換的一些個人理解,如有不對,歡迎指正,虛心向大神們請教。
首先是從子類向父類的向上轉換。向上轉換比較直觀,總是能夠將一個子類的例項轉換為一個父類的物件,從繼承鏈的角度,這個特性很容易理解:繼承是一種“是一種”的關係,從父類派生出的子類,我們都能理解為,子類總是父類的一個例項。比如說,Fruit類派生出了Orange類,Apple類,Orange和Apple都是Fruit;Animal類派生出了Tiger類和Lion類,Tiger和Lion都是Animal。因此,從子類向父類的轉換不需要什麼限制,只需直接將子類例項賦值給父類變數即可
//Test.java
public class Test {
public static void main(String args[]) {
Animal tiger = new Tiger();
Animal lion = new Lion();
}
}
class Animal {
String name;
Animal() {
name = "animal";
}
Animal(String name) {
this.name = name;
}
}
class Tiger extends Animal {
Tiger() {
super("tiger");
}
}
class Lion extends Animal {
Lion() {
super("lion");
}
}
然而從父類向子類的向下轉換就稍微複雜一些了。在講述向下轉換之前,也許有些剛學java的朋友會有點不解為什麼要使用向下轉換,使用多型和動態繫結機制通過父型別變數使用子變數不就可以了麼(比如我就曾對此感到疑惑)。這就要考慮到,在繼承關係中,有一些方法是不適合由父類定義並由子類繼承並重寫的,有些方法是子類特有的,不應該通過繼承得到,且子類可能也會有自己特有的成員變數
從父類向子類的轉換就有限制了。首先,父類變數向子類轉換必須通過顯式強制型別轉換,採取和向上轉換相同的直接賦值方式是不行的,;並且,當把一個父型別變數例項轉換為子型別變數時,必須確保該父類變數是子類的一個例項,從繼承鏈的角度來理解這些原因:子類一定是父類的一個例項,然而父類卻不一定是子類的例項;比如說,Fruit未必是Orange,它可能是Apple;Animal也不一定是Tiger,它可能是Lion。用程式碼來解釋一下:
Animal tiger = new Tiger();
Animal lion = new Lion();
在前面向上轉換的程式碼示例當中,main方法中的這兩行程式碼,意思就是父型別變數tiger是子類Tiger的一個例項,lion是Lion的一個例項。
也就是說,如果要把tiger轉換為Tiger型別,必須保證tiger本身是Tiger的一個例項,在上例中,如果要把tiger轉換成Lion型別,或是把Lion型別轉換為Tiger型別,都是行不通的,在執行時,這會丟擲一個執行異常ClassCastException,表示類轉換異常。因此,在進行父類向子類的轉換時,一個好的習慣是通過instanceof運算子來判斷父類變數是否是該子類的一個例項:
Tiger t = null;
if(tiger instanceof Tiger)
t = (Tiger)tiger;
如果要通過父類呼叫子類變數的方法,那麼要注意要將父型別變數和強制轉換用括號括起來:
Number i = new Integer(3);
System.out.println(
((Integer)i).compareTo(new Integer(4))
);
因為成員訪問運算子.的優先順序大於型別轉換,所以要用括號括起來保證型別轉換先於成員訪問進行運算。
前面說到用instanceof判斷父類是否是子類的一個例項是一個好習慣,如果不養成這個習慣的話很容易出問題,請看下面這段程式碼:
Animal animal = new Tiger();
Lion lion = (Lion)tiger;
前面說到,這段程式碼會在執行時丟擲ClassCastException異常,然而,這段程式碼卻是能夠編譯成功的。原因是因為,Java編譯器並沒有聰明到能夠在編譯階段就知道父型別變數是哪一個子類的例項,所以,將animal轉換為Lion型別的程式碼:(Lion)animal是能夠編譯通過的,即使事實上我們能看到animal是Tiger的一個例項,因為Animal型別確實能轉換成Lion型別,所以這條語句是合法的。所以,如果沒有使用instanceof防止不同子型別之間的物件轉換,而又不能指望編譯器檢查出這種轉換邏輯錯誤的話,就很容易犯錯了。
一、父類引用指向子類物件時
1、若子類覆蓋了某方法,則父類引用呼叫子類重新定義的新方法
2、若子類未覆蓋某方法,則父類引用呼叫父類本身的舊方法
3、若子類覆蓋了某屬性,但父類引用仍呼叫父類本身的舊屬性
4、若子類未覆蓋某屬性,則父類引用呼叫父類本身的舊屬性
5、父類引用不能訪問子類新定義的屬性和方法
二、子類引用指向自身物件時
1、若子類覆蓋了某方法,則子類引用呼叫子類重新定義的新方法
2、若子類未覆蓋某方法,則子類引用呼叫父類本身的舊方法
3、若子類覆蓋了某屬性,則子類引用呼叫子類重新定義的新屬性
4、若子類未覆蓋某屬性,則子類引用呼叫父類本身的舊屬性
5、子類引用可以訪問子類新定義的方法三、示例程式碼
B.java- publicclass B {
- int a = 1;
- int b = 2;
- void f1() {
- System.out.println("B.f1()");
- }
- void f2() {
- System.out.println("B.f2()");
- }
- }
- publicclass C extends B {
- int a = 3;
- @Override
- void f1() {
- System.out.println("C.f1()");
- }
- void f3() {
- System.out.println("C.f3()");
- }
- publicstaticvoid main(String[] args) {
- B b = new C();// 父類引用指向子類物件
- b.f1();// 子類覆蓋了該方法,所以父類引用呼叫新方法
- b.f2();// 子類未覆蓋該方法,所以父類引用呼叫舊方法
- // b.f3();此行去掉註釋會報錯,父類引用不能訪問子類新定義方法
- System.out.println(b.a);// 子類覆蓋了該屬性,但父類引用仍舊訪問舊屬性
- System.out.println(b.b);// 子類未覆蓋該屬性,父類訪問舊屬性
- System.out.println();
- C c = new C();// 子類引用指向自身物件
- c.f1();// 子類覆蓋了父類方法,所以呼叫新方法
- c.f2();// 子類未覆蓋父類方法,所以呼叫舊方法
- c.f3();// 子類呼叫自己新定義的方法
- System.out.println(c.a);// 子類覆蓋了該屬性,所以訪問新屬性
- System.out.println(c.b);// 子類未覆蓋該屬性,所以訪問舊屬性
- }
- }
輸出:
- C.f1()
- B.f2()
- 1
- 2
- C.f1()
- B.f2()
- C.f3()
- 3
- 2