向上類型轉換
對于引用變量,在程序中有兩種形態:一種是編譯時類型,這種引用變量的類型在聲明它的時候就決定了;另一種則是運行時類型,這種變量的類型由實際賦給它的對象決定。
當一個引用變量的編譯時類型和運行時類型不一致時,就出現了多態(Polymorphism)
對面向對象語言來說,所有的對象(Object),或者說類的實例,本質上都是引用變量。因此,多態最主要就是針對對象來說的:聲明時引用變量指向的對象的類型,和運行時引用變量指向的對象的類型不一致。
class BaseClass
{public int book = 6;public void base(){System.out.println("父類的普通方法");}public void test(){System.out.println("父類被覆蓋的方法");}
}public class SubClass extends BaseClass
{// 覆蓋public String book = "Java瘋狂講義"; // 同名實例public void test(){System.out.println("子類的覆蓋父類的方法");}public void sub(){System.out.println("子類的普通方法");}public static void main(String[] args){var bc = new BaseClass(); // 聲明一個BaseClass的對象,編譯時和運行時的類型一致,不存在多態System.out.println(bc.book); // 6bc.base(); // 父類的方法bc.test(); // 父類的方法//-------------var sc = new SubClass(); // 聲明一個SubClass的對象,同樣不存在多態System.out.println(sc.book); // 子類實例變量覆蓋了父類的實例變量,輸出"Java瘋狂講義"sc.base(); // 子類方法覆蓋了父類的方法sc.test(); // 子類的普通方法//-------------BaseClass polymophicBC = new SubClass(); // 編譯時類型是BaseClass,運行時類型是SubClass,發生了多態System.out.println(polymophicBC.book); // 輸出6,是父類的實例變量polymophicBC.base(); // 執行父類的base方法polymophicBC.test(); // 執行當前類,也就是運行時類型SubClass的test方法// polymophicBC的編譯時類型是BaseClass,沒有提供sub方法// 因此調用sub方法時會出現編譯錯誤// polymophicBC.sub();}
}
- 28~31行是標準的對象的聲明與使用;
- 33~36行是標準的繼承;
- 38~41行出現了多態。
對變量
polymophicBC
來說,編譯時類型是BaseClass
(聲明語句左端),運行時類型是SubClass
(聲明語句右端)。把一個子類對象賦給一個父類引用變量,在這個過程中發生了什么?
類型轉換。
多態在Java中實現的機制就是把子類對象賦值給父類引用變量,這實際上就是一種類型裝換,具體也叫向上轉型(up-casting)。這種類型轉換由系統自動完成。
從聲明語句左邊來看:
- BaseClass bc = new BaseClass();
- BaseClass polymophicBC = new SubClass();
bc
和polymophicBC
都是BaseClass
引用類型,但是它倆在執行同名函數test()
時卻產生了不同的結果。這種調用同一個方法卻出現不同行為特征的現象,就是多態。
多態機制下,父類引用變量在運行時總是調用子類的方法,也就是說呈現出子類的行為特征而不是父類的行為特征。
對象的實例變量不具有多態性。
- 第39行,
book
仍然是父類的實例變量。
引用變量在編譯階段只能調用其編譯時類型擁有的方法,但是在運行時可以執行其運行時類型擁有的方法。
- 第44行,
BaseClass
不具有sub()
方法,因此不能調用,發生編譯錯誤; - 但第41行,
BaseClass
具有test()
方法,因此可以調用,且在運行時執行的是SubClass
的同名方法。
使用
var
時,并不能改變編譯時類型,因此也可能會發生多態:
var v1 = new SubClass(); // 自動推斷是SubClass,沒有多態
var v2 = polymophicBC(); // 賦值,v2自動推斷是BaseClass
// 此時調用sub方法,遵照多態機制,會發生編譯錯誤
// v2.sub();
強制類型轉換
按照上面規則,引用變量只能調用編譯時類型擁有的方法,即使它的運行時類型對象實際上包含了遠不止這些方法。
如何讓這個引用變量調用運行時類型所擁有的方法呢?
既然普通的多態依賴的是向上轉型,即把子類對象賦給父類引用變量,類似于我們把double基本變量賦給float。那么也可以反過來,執行強制類型轉換。
強制類型轉換借助類型轉換運算符,和C++類似,就是()
。
類型轉換運算符可以實現基本類型之間的轉型,也能實現引用變量的轉型。
請注意,強制類型轉換不是萬能的,受到如下約束:
- 基本類型之間轉型只能在數值類型中進行(整數型、字符型、浮點型)。數值型和布爾型之間不能轉換(C++中是可以的)。
- 引用類型轉換只能在具有繼承關系(直接繼承或間接繼承都行)的類型之間進行。
強制類型轉換在這里,就是把父類實例轉換為子類類型。即其編譯時類型是父類類型,運行時類型是子類類型。這時候可以使用強制類型轉換。
public class ConversionTest
{public static void main(String[] args){var d = 13.4; // floatvar l = (long) d; // 強制類型轉換//------var in = 5; // int// var b = (boolean) in; // 錯誤,數值型不能轉換為布爾型//------Object obj = "Hello"; // 向上轉型,"Hello"是String,是Object的子類。這實際上就是多態,只不過這時候obj不能執行String擁有的方法var objStr = (String) obj; // 強制類型轉換,父類/基類和子類,正常System.out.println(objStr); // 做為String類型輸出//------Object objPri = Integer.valueOf(5); // 向上轉型,運行時類型是Integervar in = (Integer) objPri; // 強制類型轉換,基類和子類,正常// var str = (String) objPri; // objPri運行時時Integer,和String不存在繼承關系,運行時會報錯(類型轉換異常,ClassCastException)}
}
再解讀一下第12行:
- 對
objStr
,雖然使用了var
,但由于使用了強制類型轉換符(String)
,自動推斷它是String
類型;- 此時
obj
是Object
類型;- 因此,將
obj
賦值給objStr
,實際上是把父類對象賦值給子類引用變量,這就和之前的upcasting正好相反,我們也可以稱之為downcasting
小結
- 把子類對象(右)賦給父類引用變量(左)時,觸發向上轉型,這種轉型是自動的、總是成功的。這種轉型表明這個引用變量編譯時是父類類型,運行時是子類類型。它表現出的是子類的行為方式,但是編譯時不能調用子類的方法。同時,實例變量仍然是父類的。
- 使用強制類型轉換可以把一個引用變量轉換成其子類類型。這種轉換必須是顯式的,而且不一定成功(若兩端不存在繼承關系)。
instanceof
使用instanceof
運算符可以判斷是否可以執行類型轉換,以避免出現ClassCastException
:
if (objPri instanceof String)
{var str = (String) objPri;
}
instanceof
用來判斷前面的對象是否是后面的類或者其子類的實例,是的話返回true
,否則返回false
。
在Java 17中,為instanceof
增加了快捷用法,來簡化上面的判斷代碼塊:
// 傳統instanceof,先判斷,再轉換,最后使用
if (obj instanceof String) // 先判斷
{var s = (String) obj; // 再轉換System.out.println(s.toUpperCase()); // 最后使用
}// Java 17的模式匹配,同時完成判斷和類型轉換
if (obj instanceof String s)
{System.out.println(s.toUpperCase());
}