Java類型信息rtti

53202955 8年前發布 | 11K 次閱讀 Java Java開發

 

RTTI Run-Time Type Infomation(運行時類型信息),在Java運行時,RTTI維護類的相關信息,識別類和對象的信息。 多態(polymorphism)是基于RTTI實現的。RTTI的功能主要是由Class類實現的。

嚴格的說,反射也是一種形式的RTTI,不過,一般的文檔資料中把RTTI和反射分開,因為一般的,大家認為RTTI指的是傳統的RTTI ,通過繼承和多態來實現,在運行時通過調用超類的方法來實現具體的功能(超類會自動實例化為子類,或使用instance of)。

傳統的RTTI有3種實現方式:

1.向上轉型或向下轉型(upcasting and downcasting),在java中,向下轉型(父類轉成子類)需要強制類型轉換。

2.Class對象(用了Class對象,不代表就是反射,如果只是用Class對象cast成指定的類,那就還是傳統的RTTI)。

3.instanceof或isInstance()。

其中cast的用法如下所示:

public class Test1 {
    public static void main(String[] args) throws ClassNotFoundException {
        Father f = new Child();
        Class<Child> cc = Child.class ;
        Child c = cc.cast(f) ; 
    }
}
class Father {
}
class Child extends Father {
}
    

這里 Child c = cc.cast(f) ; 等價于 Child c = (Child)f ;

傳統的RTTI與反射最主要的區別:RTTI,編譯器在編譯時打開和檢查.class文件。 而反射不需要,反射在運行時打開和檢查.class文件。傳統的RTTI使用轉型或Instance形式實現, 但都需要指定要轉型的類型,比如:

public void rtti(Object obj){
    Toy toy = Toy(obj);
    // Toy toy = Class.forName("com.rtti.Toy")
    // obj instanceof Toy
}

注意其中的obj雖然是被轉型了,但在編譯期,就需要知道要轉成的類型Toy,也就是需要Toy的.class文件。 相對的,反射完全在運行時在通過Class類來確定類型,不需要提前加載Toy的.class文件。

Class類

Class類是”類的類”(class of classes)。如果說類是對象的抽象和集合的話,那么Class類就是對類的抽象和集合。 每一個Class類的對象代表一個其他的類。比如下面的程序中,Class類的對象c1代表了Father類,c2代表了Child類。

public class Test1 {
    public static void main(String[] args)
    {
        Father father = new Father();
        Class c1  = father.getClass();
        System.out.println(c1.getName());
        Father child = new Child();
        Class c2  = child.getClass();
        System.out.println(c2.getName());
    }
}
class Father {
}
class Child extends Father {
}
    

當我們調用對象的getClass()方法時,就得到對應Class對象的引用。 在c2中,即使我們將child對象的引用向上轉換為Father對象的引用,對象所指向的Class類對象依然是Child。

Java中每個對象都有相應的Class類對象,因此,我們隨時能通過Class對象知道某個對象“真正”所屬的類。無論我們對引用進行怎樣的類型轉換, 對象本身所對應的Class對象都是同一個。當我們通過某個引用調用方法時,Java總能找到正確的Class類中所定義的方法,并執行該Class類中的代碼。 由于Class對象的存在,Java不會因為類型的向上轉換而迷失。這就是多態的原理。

除了getClass()方法外,我們還有其他方式調用Class類的對象。

public class Test1 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class c1  = Class.forName("com.souly.myapplication.test.Father");
        System.out.println(c1.getName());
        Class c2  = Child.class ;
        System.out.println(c2.getName());
    }
}
class Father {
}
class Child extends Father {
}
  

上面顯示了兩種方式:

1.forName()方法接收一個字符串作為參數,該字符串是類的名字。這將返回相應的Class類對象。 2.Child.class方法是直接調用類的class成員。這將返回相應的Class類對象。

使用forName(String str)有一個副作用:如果類沒有被加載,調用它會觸發類的static子句(靜態初始化塊)。與之相比,更好用的是類字面常量, 例如Child.class。支持編譯時檢查,所以不會拋出異常。使用類字面常量創建Class對象的引用與forName(String str) 不同,不會觸發類的static子句(靜態初始化塊)。所以,更簡單更安全更高效。類字面常量支持類、接口、數組、基本數據類型。

Class類的加載

Java程序在運行之前并沒有被完全加載,各個部分是在需要時才被加載的。

為了使用類而作的準備包含三步:

1.加載。由classloader查找class字節碼文件,創建一個Class對象。 2.鏈接:驗證字節碼文件,為靜態域分配存儲空間,如果必需的話,會解析這個類創建的對其他類的所有引用(比如說該類持有static域)。 3.初始化:初始化父類,執行靜態初始化器和靜態初始化塊。

其中靜態初始化器可以理解為靜態域在定義處的初始化,如:static Dog d = new Dog(0);。

final static成員和static成員的在以上三個過程中不一樣,final static成員被稱為“編譯器常量”,在編譯時已經被賦值, 所以可以在類加載前就進行訪問,而靜態成員(非final)需要在類加載后、class對象初始化之后賦值。

當Java創建某個類的對象,jvm虛擬機的classLoader會檢測對象對應的Class對象是否已加載, 比如Child類對象時,Java會檢查內存中是否有相應的Class對象。如果內存中沒有相應的Class對象,會依據相關途徑查詢對應.class文件 (如 通過classPath在本地文件系統進行查找,在獲取到.class文件之后會對文件進行有效驗證,之后會依據Class對象進行詳細類型對象的創建。 在Class對象加載成功后,其他Child對象的創建和相關操作都將參照該Class對象。

更詳細的介紹可以參考:

Java 類加載與初始化

舉例說明

interface HasBatteries {}
interface Waterproof {}
interface Shoots {}
class Toy {
    Toy() {}
    Toy(int i) {}
}
class FancyToy extends Toy implements HasBatteries, Waterproof, Shoots {
    FancyToy() { super(1); }
}
public class ToyTest {
    static void printInfo(Class cc) {
        System.out.println("Class name: " + cc.getName() +
                " is interface? [" + cc.isInterface() + "]");
    }
    public static void main(String[] args) {
        Class c = null;
        try {
            c = Class.forName("com.souly.myapplication.test.FancyToy");
        } catch(ClassNotFoundException e) {
            System.out.println("Can't find FancyToy");
            System.exit(1);
        }
        printInfo(c);
        Class[] faces = c.getInterfaces();
        for(int i = 0; i < faces.length; i++)
            printInfo(faces[i]);
        Class cy = c.getSuperclass();
        Object o = null;
        try {
            // Requires default constructor:
            o = cy.newInstance(); // (*1*)
        } catch(InstantiationException e) {
            System.out.println("Cannot instantiate");
            System.exit(1);
        } catch(IllegalAccessException e) {
            System.out.println("Cannot access");
            System.exit(1);
        }
        printInfo(o.getClass());
    }
}
    

運行后輸出結果如下:

   Class name: com.souly.myapplication.test.FancyToy is interface? [false]
   Class name: com.souly.myapplication.test.HasBatteries is interface? [true]
   Class name: com.souly.myapplication.test.Waterproof is interface? [true]
   Class name: com.souly.myapplication.test.Shoots is interface? [true]
   Class name: com.souly.myapplication.test.Toy is interface? [false]
    

從中可以看出,class FancyToy相當復雜,因為它從Toy中繼承,并實現了HasBatteries,Waterproof以及ShootsThings的接口。在main()方法中 有一個Class對象,它通過Class.forName()初始化成FancyToy Class。Class.getInterfaces方法會返回Class對象的一個數組,用于表示Class對象內的接口。 若有一個Class對象,也可以用getSuperclass()查詢該對象的直接父類是什么。當然,這種做會返回一個Class對象,可用它作進一步的查詢。 這意味著在運行期的時候,完全有機會調查到對象的完整層次結構。

若從表面看,Class的newInstance()方法似乎是克隆(clone())一個對象的另一種手段。但兩者是有區別的。利用newInstance(), 我們可在沒有現成對象供“克隆”的情況下新建一個對象。就像上面的程序演示的那樣,當時沒有Toy對象,只有cy——即Toy的Class對象。 利用它可以實現“虛擬構建器”。在上述例子中,cy只是一個Class對象,編譯期間并不知道進一步的類型信息。一旦新建了一個實例后, 可以得到Object對象。但那是一個Toy對象。

用newInstance()創建的類必須有一個無參數的構造方法。沒有辦法用newInstance()創建擁有非默認構建方法的對象,如果我們注釋掉Toy() {}構造方法, 將會報錯:java.lang.InstantiationException。

參考閱讀:

Thinking in Java中對RTTI的介紹

 

來自: http://souly.cn/技術博文/2016/04/23/java類型信息RTTI/

 本文由用戶 53202955 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!