1. 程式人生 > >【深入JAVA】RTTI與反射

【深入JAVA】RTTI與反射

為什麽 觸發 UC 上下 ron keyword open 基本上 public

有Java中,我們如何在運行時識別類和對象的信息?有兩種方法,一是傳統的RTTI,另一種是反射。

1.RTTI Run-Time Type Infomation 運行時類型信息

為什麽需要RTTI?

越是優秀的面向對象設計,越是強調高內聚低耦合,正如依賴倒轉原則所說:“無論是高層模塊還是低層模塊,都應該針對抽象編程”。

比如說我們有一個抽象父類:

Shape draw()

以下是三個具體類:

Circle draw()
Square draw()
Triangle draw()

某些情況下,我們持有Shape,但卻遠遠不夠——因為我們想要針對它的具體類型進行特殊處理,然而我們的設計完全針對抽象,所以在當前上下文環境中無法判斷具體類型。
因為RTTI的存在,使得我們在不破壞設計的前提下得以達到目的。


Class類與Class對象

事實上,每一個類都持有其對應的Class類的對象的引用(Object類中的getClass()能讓我們獲取到它),其中包含著與類相關的信息。
非常容易註意到,針對每一個類,編譯Java文件會生成一個二進制.class文件,這其中就保存著該類對應的Class對象的信息。

.class是用於供類加載器使用的文件

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

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

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

註:原文為static initializers,經查看Thinking in Java,其意應為靜態域在定義處的初始化,如:
static Dog d = new Dog(0);

所有的類都是在對其第一次使用時,動態加載到JVM中去的。當程序創建第一個對類的靜態成員的引用時,JVM會使用類加載器來根據類名查找同名的.class——一旦某個類的Class對象被載入內存,它就被用來創建這個類的所有對象。構造器也是類的靜態方法,使用new操作符創建新對象會被當作對類的靜態成員的引用。
註意特例:如果一個static final

值是編譯期常量,讀取這個值不需要對類進行初始化。所以說對於不變常量,我們總是應該使用static final修飾。


Class.forName(String str)

Class類有一個很有用的靜態方法forName(String str),可以讓我們對於某個類不進行創建就得到它的Class對象的引用,例如這個樣子:

try {
     Class toyClass = Class.forName("com.duanze.Toy"); // 註意必須使用全限定名
} catch (ClassNotFoundException e) {

}

然而,使用forName(String str)有一個副作用:如果Toy類沒有被加載,調用它會觸發Toy類的static子句(靜態初始化塊)。

與之相比,更好用的是類字面常量,像是這樣:

Class toyClass = Toy.class;

支持編譯時檢查,所以不會拋出異常。使用類字面常量創建Class對象的引用與forName(String str)不同,不會觸發Toy類的static子句(靜態初始化塊)。所以,更簡單更安全更高效。
類字面常量支持類、接口、數組、基本數據類型。


×拓展×

Class.forName(String className)使用裝載當前類的類裝載器來裝載指定類。因為class.forName(String className)方法內部調用了Class.forName(className, true, this.getClass().getClassLoader())方法,如你所見,第三個參數就是指定類裝載器,顯而易見,它指定的是裝載當前類的類裝載器的實例,也就是this.getClass().getClassLoader();

你可以選擇手動指定裝載器:

ClassLoader cl = new  ClassLoader();   
Class c1 = cl.loadClass(String className, boolean resolve );  

更詳細的參考


範化的Class引用

通過範型以及通配符,我們能對Class對象的引用進行類型限定,像是:

Class<Integer> intClass = int.class; // 註意右邊是基本數據類型的類字面常量

這樣做的好處是能讓編譯器進行額外的類型檢查。
知道了這一點以後,我們可以把之前的例子改寫一下:

Class toyClass = Toy.class;
Class<?> toyClass = Toy.class;

雖然這兩句是等價的,但從可讀性來說Class<?>要優於Class,這說明編程者並不是由於疏忽而選擇了非具體版本,而是特意選擇了非具體版本。


Class.newInstance()

既然拿到了包含著類信息的Class對象的引用,我們理應可以構造出一個類的實例。Class.newInstance()就是這樣一個方法,比如:

// One
try {
    Class<?> toyClass = Class.forName("com.duanze.Toy"); 
    Object obj = toyClass.newInstance();
} catch (ClassNotFoundException e) {

}

// Two
Class<?> toyClass = Toy.class;
Object obj = toyClass.newInstance();

使用newInstance()創建的類,必須帶有默認構造器。
由於toyClass僅僅只是一個Class對象引用,在編譯期不具備更進一步的類型信息,所以你使用newInstance()時只會得到一個Object引用。如果你需要拿到確切類型,需要這樣做:

Class<Toy> toyClass = Toy.class;
Toy obj = toyClass.newInstance();

但是,如果你遇到下面的情況,還是只能拿到Object引用:

Class<SubToy> subToyClass = SubToy.class;
Class<? super SubToy> upClass = subToy.getSuperclass(); // 希望拿到SubToy的父類Toy的Class對象引用
// This won‘t compile:
// Class<Toy> upClass = subToy.getSuperclass();
// Only produces Object:
Object obj = upClass.newInstance();

雖然從常理上來講,編譯器應該在編譯期就能知道SubToy的超類是Toy,但實際上卻並不支持這樣寫:

// This won‘t compile:
Class<Toy> upClass = subToy.getSuperclass();

而只能夠接受:

Class<? super SubToy> upClass = subToy.getSuperclass(); // 希望拿到SubToy的父類Toy

這看上去有些奇怪,但現狀就是如此,我們惟有接受。好在這並不是什麽大問題,因為轉型操作並不困難。


類型檢查

在進行類型轉換之前,可以使用instanceof關鍵字進行類型檢查,像是:

if ( x instanceof Shape ) {
     Shape s = (Shape)x;
}

一般情況下instanceof已經夠用,但有些時候你可能需要更動態的測試途徑:Class.isInstance(Class clz):

Class<Shape> s = Shape.class;
s.isInstance(x);

可以看到,與instanceof相比,isInstance()的左右兩邊都是可變的,這一動態性有時可以讓大量包裹在if else...中的instanceof縮減為一句。


2.反射

不知道你註意到了沒有,以上使用的RTTI都具有一個共同的限制:在編譯時,編譯器必須知道所有要通過RTTI來處理的類。

但有的時候,你獲取了一個對象引用,然而其對應的類並不在你的程序空間中,怎麽辦?(這種情況並不少見,比如說你從磁盤文件或者網絡中獲取了一串字串,並且被告知這一串字串代表了一個類,這個類在編譯器為你的程序生成代碼之後才會出現。)

Class類和java.lang.reflect類庫一同對反射的概念提供了支持。反射機制並沒有什麽神奇之處,當通過反射與一個未知類型的對象打交道時,JVM只是簡單地檢查這個對象,看它屬於哪個特定的類。因此,那個類的.class對於JVM來說必須是可獲取的,要麽在本地機器上,要麽從網絡獲取。所以對於RTTI和反射之間的真正區別只在於:

  • RTTI,編譯器在編譯時打開和檢查.class文件
  • 反射,運行時打開和檢查.class文件

明白了以上概念後,什麽getFields(),getMethods(),getConstructors()之類的方法基本上全都可以望文生義了。

我們可以看一下Android開發中經常用的對於ActionBar,讓Overflow中的選項顯示圖標這一效果是怎麽做出來的:

/*
overflow中的Action按鈕應不應該顯示圖標,
是由MenuBuilder這個類的setOptionalIconsVisible方法來決定的,
如果我們在overflow被展開的時候給這個方法傳入true,
那麽裏面的每一個Action按鈕對應的圖標就都會顯示出來了。
*/

@Override
public boolean onMenuOpened(int featureId, Menu menu) {
    if (featureId == Window.FEATURE_ACTION_BAR && menu != null) {
        if (menu.getClass().getSimpleName().equals("MenuBuilder")) {
            try {
                // Boolean.TYPE 同 boolean.class
                Method m = menu.getClass().getDeclaredMethod("setOptionalIconsVisible", Boolean.TYPE); 
                // 通過setAccessible(true),確保可以調用方法——即使是private方法
                m.setAccessible(true);
                // 相當於:menu.setOptionalIconsVisible(true) 
                m.invoke(menu, true);
            } catch (Exception e) {
            }
        }
    }
    return super.onMenuOpened(featureId, menu);
}
總結:
如果不知道一個對象的準確類型,RTTI會幫助我們調查。但卻有一個限制:類型必須是在編譯期間已知的。而反射使我們能在運行期間探察一個類,RTTI和“反射”之間唯一的區別就是:對RTTI來說,編譯器會在
編譯期打開和檢查.class文件。但對“反射”來說,.class文件在編譯期間是不可使用的,而是由運行時環境打開和檢查 ,我們利用反射機制一般是使用java.lang.reflect包提供給我們的類和方法。

【深入JAVA】RTTI與反射