Java入門記(二):向上轉型與向下轉型


  在對Java學習的過程中,對於轉型這種操作比較迷茫,特總結出了此文。例子參考了《Java編程思想》。

    目錄

幾個同義詞

向上轉型與向下轉型

  例一:向上轉型,調用指定的父類方法

  例二:向上轉型,動態綁定

  例三:向上轉型,靜態綁定

  例四:向下轉型

轉型的誤區

  1.運行信息(RTTI)

  2.數組類型

  3.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>在運行期是同一種類型,但是具體能持有的元素類型會在編譯期進行檢查。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM