【深入JAVA】RTTI與反射
有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程序在運行之前並沒有被完全加載,各個部分是在需要時才被加載的。
為了使用類而作的準備包含三步:
- 加載。由類加載器執行,查找字節碼,創建一個
Class
對象。 - 鏈接。驗證字節碼,為靜態域分配存儲空間,如果必需的話,會解析這個類創建的對其他類的所有引用(比如說該類持有
static
- 初始化。如果該類有超類,則對其初始化,執行靜態初始化器[註]和靜態初始化塊。
註:原文為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與反射