「深入Java」类型信息:RTTI和反射
1.RTTI Run-Time Type Infomation 運行時類型信息
為什么需要RTTI?
越是優秀的面向對象設計,越是強調高內聚低耦合,正如依賴倒轉原則所說:“無論是高層模塊還是低層模塊,都應該針對抽象編程”。
比如說我們有一個抽象父類:
| 1 | Shape draw() |
以下是三個具體類:
| 1 2 3 | Circle draw() Square draw() Triangle draw() |
某些情況下,我們持有Shape,但卻遠遠不夠——因為我們想要針對它的具體類型進行特殊處理,然而我們的設計完全針對抽象,所以在當前上下文環境中無法判斷具體類型。
因為RTTI的存在,使得我們在不破壞設計的前提下得以達到目的。
Class類與Class對象
事實上,每一個類都持有其對應的Class類的對象的引用(Object類中的getClass()能讓我們獲取到它),其中包含著與類相關的信息。
非常容易注意到,針對每一個類,編譯Java文件會生成一個二進制.class文件,這其中就保存著該類對應的Class對象的信息。
.class是用于供類加載器使用的文件
Java程序在運行之前并沒有被完全加載,各個部分是在需要時才被加載的。
為了使用類而作的準備包含三步:
注:原文為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對象的引用,例如這個樣子:
| 1 2 3 4 5 | try { ?????Class toyClass = Class.forName("com.duanze.Toy"); // 注意必須使用全限定名 } catch (ClassNotFoundException e) { } |
然而,使用forName(String str)有一個副作用:如果Toy類沒有被加載,調用它會觸發Toy類的static子句(靜態初始化塊)。
與之相比,更好用的是類字面常量,像是這樣:
| 1 | 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();
你可以選擇手動指定裝載器:
| 1 2 | ClassLoader cl = new? ClassLoader();?? Class c1 = cl.loadClass(String className, boolean resolve ); |
更詳細的參考
范化的Class引用
通過范型以及通配符,我們能對Class對象的引用進行類型限定,像是:
| 1 | Class<Integer> intClass = int.class; // 注意右邊是基本數據類型的類字面常量 |
這樣做的好處是能讓編譯器進行額外的類型檢查。
知道了這一點以后,我們可以把之前的例子改寫一下:
| 1 2 | Class toyClass = Toy.class; Class<?> toyClass = Toy.class; |
雖然這兩句是等價的,但從可讀性來說Class<?>要優于Class,這說明編程者并不是由于疏忽而選擇了非具體版本,而是特意選擇了非具體版本。
Class.newInstance()
既然拿到了包含著類信息的Class對象的引用,我們理應可以構造出一個類的實例。Class.newInstance()就是這樣一個方法,比如:
| 1 2 3 4 5 6 7 8 9 10 11 | // 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引用。如果你需要拿到確切類型,需要這樣做:
| 1 2 | Class<Toy> toyClass = Toy.class; Toy obj = toyClass.newInstance(); |
但是,如果你遇到下面的情況,還是只能拿到Object引用:
| 1 2 3 4 5 6 | 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,但實際上卻并不支持這樣寫:
| 1 2 | // This won't compile: Class<Toy> upClass = subToy.getSuperclass(); |
而只能夠接受:
| 1 | Class<? super SubToy> upClass = subToy.getSuperclass(); // 希望拿到SubToy的父類Toy |
這看上去有些奇怪,但現狀就是如此,我們惟有接受。好在這并不是什么大問題,因為轉型操作并不困難。
類型檢查
在進行類型轉換之前,可以使用instanceof關鍵字進行類型檢查,像是:
| 1 2 3 | if ( x instanceof Shape ) { ?????Shape s = (Shape)x; } |
一般情況下instanceof已經夠用,但有些時候你可能需要更動態的測試途徑:Class.isInstance(Class clz):
| 1 2 | 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中的選項顯示圖標這一效果是怎么做出來的:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | /* 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); } |
×拓展:動態代理×
Java中對于反射的一處重要使用為動態代理,可以參考這篇IBM developerworks的文章
參考資料
- 《Java編程思想》
- http://blog.csdn.net/guolin_blog/article/details/18234477#t7
- http://yanwushu.sinaapp.com/class_forname/
總結
以上是生活随笔為你收集整理的「深入Java」类型信息:RTTI和反射的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Class对象和Java反射机制
- 下一篇: 粗浅看 java反射机制