在對Java學習的過程中,對於轉型這種操作比較迷茫,特總結出了此文。例子參考了《Java編程思想》。
目錄
幾個同義詞
首先是幾組同義詞。它們出現在不同的書籍上,這是造成理解混淆的原因之一。
父類/超類/基類
子類/導出類/繼承類/派生類
靜態綁定/前期綁定
動態綁定/后期綁定/運行時綁定
向上轉型與向下轉型
例一:向上轉型,調用指定的父類方法
class Shape {
static void draw(Shape s) { System.out.println("Shape draw."); } } class Circle extends Shape {
static void draw(Circle c) { System.out.println("Circle draw."); } } public class CastTest { public static void main(String args[]) { Circle c = new Circle(); Shape.draw(c); } }
輸出為
Shape draw.
這表明,draw(Shape s)方法本來被設計為接受Shape引用,但這里傳遞的是Circle引用。實際上draw(Shape s)方法可以對所有Shape類的導出類使用,這被稱為向上轉型。表現的行為,和方法所屬的類別一致。換句話說,由於明確指出是父類Shape的方法,那么其行為必然是這個方法對應的行為,沒有任何歧義可言。
“向上轉型”的命名來自於類繼承圖的畫法:根置於頂端,然后逐漸向下,以本例中兩個類為例,如下圖所示:
例二:向上轉型,動態綁定
class Shape { public void draw() { System.out.println("Shape draw."); } } class Circle extends Shape { public void draw() { System.out.println("Circle draw."); } } public class CastTest { public static void drawInTest(Shape s) { s.draw(); } public static void main(String args[]) { Circle c = new Circle(); drawInTest(c); } }
輸出為
Circle draw.
這樣做的原因是,一個drawInTest(Shape s)就可以處理Shape所有子類,而不必為每個子類提供自己的方法。但這個方法能能調用父類和子類所共有的方法,即使二者行為不一致,也只會表現出對應的子類方法的行為。這是多態所允許的,但容易產生迷惑。
例三:向上轉型,靜態綁定
class Shape { public static void draw() { System.out.println("Shape draw."); } } class Circle extends Shape { public static void draw() { System.out.println("Circle draw."); } } public class CastTest { public static void drawInTest(Shape s) { s.draw(); } public static void main(String args[]) { Circle c = new Circle(); drawInTest(c); } }
輸出為
Shape draw.
例三與例二有什么區別?細看之下才會發現,例三里調用的方法被static修飾了,得到了完全不同的結果。
這兩例行為差別的原因是:Java中除了static方法和final方法(包括private方法),其他方法都是動態綁定的。對於一個傳入的基類引用,后期綁定能夠正確的識別其所屬的導出類。加了static,自然得不到這個效果了。
了解了這一點之后,就可以明白為什么要把例一寫出來了。例一中的代碼明確指出調用父類方法,而例三調用哪個方法是靜態綁定的,不是直接指明的,稍微繞了一下。
例四:向下轉型
出自《Java編程思想》8.5.2節,稍作了修改,展示如何通過類型轉換獲得子類獨有方法的訪問方式。
這相當於告訴了編譯器額外的信息,編譯器將據此作出檢查。
class Useful { public void f() {System.out.println("f() in Useful");} public void g() {System.out.println("g() in Useful");} } class MoreUseful extends Useful { public void f() {System.out.println("f() in MoreUseful");} public void g() {System.out.println("g() in MoreUseful");} public void u() {System.out.println("u() in MoreUseful");} } public class RTTI { public static void main(String[] args) { Useful[] x = { new Useful(), new MoreUseful() }; x[0].f(); x[1].g(); // Compile-time: method not found in Useful: //! x[1].u(); ((MoreUseful)x[1]).u(); // Downcast/RTTI ((MoreUseful)x[0]).u(); // Exception thrown } }
輸出
Exception in thread "main" java.lang.ClassCastException: Useful cannot be cast to MoreUseful
at RTTI.main(RTTI.java:44)
f() in Useful
g() in MoreUseful
u() in MoreUseful
雖然父類Useful類型的x[1]接收了一個子類MoreUseful對象的引用,但仍然不能直接調用其子類中的u()方法。如果需要調用,需要做向下轉型。這種用法很常見,比如一個通用的方法,處理的入參是一個父類,處理時根據入參的類型信息轉化成對應的子類使用不同的邏輯處理。
此外,父類對象不能向下轉換成子類對象。
向下轉型的好處,在學習接口時會明顯地體會出來(如果把實現接口看作多重繼承)。可以參考9.4節的例子,這里不做詳述:
interface CanFight { void fight(); } interface CanSwim { void swim(); } interface CanFly { void fly(); } class ActionCharacter { public void fight() {} } class Hero extends ActionCharacter implements CanFight, CanSwim, CanFly { public void swim() {} public void fly() {} } public class Adventure { static void t(CanFight x) { x.fight(); } static void u(CanSwim x) { x.swim(); } static void v(CanFly x) { x.fly(); } static void w(ActionCharacter x) { x.fight(); } public static void main(String[] args) { Hero i = new Hero(); t(i); // Treat it as a CanFight u(i); // Treat it as a CanSwim v(i); // Treat it as a CanFly w(i); // Treat it as an ActionCharacter } }
轉型的誤區
轉型很方便,利用轉型可以寫出靈活的代碼。不過,如果用得隨心所欲而忘乎所以的話,難免要跌跟頭。下面是幾種看似可以轉型,實際會導致錯誤的情形。
1.運行信息(RTTI)
/* 本例代碼節選自《Java編程思想》14.2.2節 */ Class<Number> genericNumberClass = int.class
這段代碼是無效的,編譯不能通過,即使把int換為Integer也同樣不通過。雖然int的包裝類Integer是Number的子類,但Integer Class對象並不是Number Class對象的子類。
2.數組類型
/* 代碼節改寫《Java編程思想》15.8.2節,本例與泛型與否無關。 */ class Generic<T> {} public class ArrayOfGeneric { static final int SIZE = 100; static Generic<Integer>[] gia; @SuppressWarnings("unchecked") public static void main(String[] args) { //! gia = (Generic<Integer>[]) new Object[SIZE]; gia = (Generic<Integer>[]) new Generic[SIZE]; } }
注釋部分在去掉注釋后運行會提示java.lang.ClassCastException。這里令人迷惑的地方在於,子類數組類型不是父類數組類型的子類。在異常提示的后面可以看到
[Ljava.lang.Object; cannot be cast to [LGeneric;
除了通過控制台輸出的異常信息,可以使用下面的代碼來看看gia究竟是什么類型:
Object[] obj = new Object[SIZE]; gia = (Generic<Integer>[]) new Generic[SIZE]; System.out.println(obj.getClass().getName()); System.out.println(gia.getClass().getName()); System.out.println(obj.getClass().getClass().getName()); System.out.println(gia.getClass().getSuperclass().getName());
控制台輸出為:
[Ljava.lang.Object;
[LGeneric;
java.lang.Object
java.lang.Object
可見,由Generic<Integer>[] gia和Object[] obj定義出的gia和obj根本沒有任何繼承關系,自然不能類型轉換,不管這個數組里是否放的是子類的對象。(子類對象是可以通過向上轉型獲得的,如果被轉換的確實是一個子類對象,見例四)
3.Java容器
/* 代碼節選自《Java編程思想》15.10節*/ class Fruit {} class Apple extends Fruit {} class Orange extends Fruit {} public class Test { public static void main(String[] args) { // 無法編譯 List<Fruit> fruitList = new ArrayList<Apple>(); } }
明明Fruit的List是可以存放Apple對象的,為什么賦值失敗?其實這根本不是向上轉型。雖然可以通過getClass().getName()得知List<Fruit>和List<Apple>同屬java.util.ArrayList類型,但是,假設這里可以編譯通過,相當於允許向ArrayList<Apple>存放一個Orange對象,顯然是不合理的。雖然由於泛型的擦除,ArrayList<Fruit>和ArrayList<Apple>在運行期是同一種類型,但是具體能持有的元素類型會在編譯期進行檢查。