1. 程式人生 > >Java 型別資訊詳解和反射機制

Java 型別資訊詳解和反射機制


> 本文部分摘自 On Java 8
## RTTI RTTI(RunTime Type Information)執行時型別資訊,能夠在程式執行時發現和使用型別資訊,把我們從只能在編譯期知曉型別資訊並操作的侷限中解脫出來 傳統的多型機制正是 RTTI 的基本使用:假設有一個基類 Shape 和它的三個子類 Circle、Square、Triangle,現在要把 Circle、Square、Triangle 物件放入 List\ 中,在執行時,先把放入其中的所有物件都當作 Object 物件來處理,再自動將型別轉換為 Shape。所有型別轉換的正確性檢查都是在執行時進行的,這也正是 RTTI 的含義所在:在執行時,識別一個物件的型別 但這樣的型別轉換並不徹底,Object 只是被轉型為 Shape,而不是更具體的 Circle、Square、Triangle,如果我們希望得到更具體的型別呢?比如說我們現在需要旋轉所有圖形,但是想跳過圓形(圓形旋轉沒有意義),這時可以使用 RTTI 查詢某個 Shape 引用所指向物件的確切型別,然後選擇進行合適的處理
## Class 物件 眾所周知,每當我們編寫並編譯了一個新類,就會產生一個 Class 物件,它包含了與類有關的資訊。我們可以使用 Class 物件來實現 RTTI,一旦某個類的 Class 物件被載入記憶體,它就可以用來建立這個類的所有物件 Class 物件都屬於 Class 型別,既然它也是物件,那我們就可以獲取和操控它的引用。forName() 是 Class 類的一個靜態方法,我們可以使用 forName() 根據目標類的全限定名(包含包名)得到該類的 Class 物件。使用 forName() 會有一個副作用,那就是如果這個類沒有被載入就會載入它,而在載入的過程中,Gum 類的 static 初始塊會被執行。當 Class.forName() 找不到要載入的類,就會丟擲異常 ClassNotFoundException ```java Class gumClass = Class.forName("Gum"); ``` 使用 Class.forName() 你不需要先持有這個型別的物件,但如果你已經擁有了目標類的物件,那就可以通過呼叫 getClass() 方法來獲取 Class 引用,這個方法來自根類 Object,它將返回表示該物件實際型別的 Class 物件的引用 ```java Gum gum = new Gum(); Class gumClass = gum.getClass(); ``` 另外,你還可以呼叫 getSuperclass() 方法來得到父類的 class 物件,再用父類的 Class 物件呼叫該方法,重複多次,你就可以得到一個完整的類繼承結構 Class 物件的 newInstance() 方法可以讓你在不知道一個的確切型別的時候建立這個類的物件,使用 newInstance() 來建立的類,必須帶有無引數的構造器 ```java Object obj = gumClass.newInstance(); ``` 當然,由於得到的是 Object 的引用,目前你只能給它傳送 Object 物件能接受的呼叫。如果你想請求具體物件才有的呼叫,你就得先獲取該物件的更多型別資訊,並執行轉型 Java 還提供了另一種生成類物件的引用:類字面常量,這樣做不僅更簡單,而且更安全,因為它在編譯時就會收到檢查(不用放在 try 語句塊中),而且根除了對 forName() 方法的呼叫,效率更高 ```java Class gumClass = Gum.class; ``` 類字面常量不僅可以用於普通類,也可以用於介面、陣列以及基本資料型別。對於基本資料型別的包裝類,還有一個標準欄位 Type,Type 欄位是一個引用,指向對應基本資料型別的 Class 物件,例如 int.class 就等價於 Integer.TYPE。還有一點值得注意的是:使用 .class 語法來獲得對類物件的引用不會觸發初始化 到這裡我們都知道了,Class 引用總是指向某個 Class 物件,而 Class 物件可以用於產生類的例項。不過自從 Java 引入泛型以後,我們就可以使用泛型對 Class 引用所指向的 Class 物件的型別進行限定,讓它的型別變得更具體些 ```java Class intClass = int.class; Class genericIntClass = int.class; intClass = genericIntClass; // 同一個東西 // genericIntClass = double.class 非法 ``` 好了,既然拿到了 Class 物件,那我們就可以這個類的型別資訊,常用的方法如下: | 方法 | 用途 | | -------------------------- | ------------------------------------------------------ | | asSubclass(Class clazz) | 把傳遞的類的物件轉換成代表其子類的物件 | | Cast | 把物件轉換成代表類或是介面的物件 | | getClassLoader() | 獲得類的載入器 | | getClasses() | 返回一個數組,陣列中包含該類中所有公共類和介面類的物件 | | getDeclaredClasses() | 返回一個數組,陣列中包含該類中所有類和介面類的物件 | | forName(String className) | 根據類名返回類的物件 | | getName() | 獲得類的完整路徑名字 | | newInstance() | 建立類的例項 | | getPackage() | 獲得類的包 | | getSimpleName() | 獲得類的名字 | | getSuperclass() | 獲得當前類繼承的父類的名字 | | getInterfaces() | 獲得當前類實現的類或是介面 |
## 型別轉換檢測 到目前為止,我們已知的 RTTI 型別包括: 1. 傳統的型別轉換,如多型 2. 代表物件型別的 Class 物件 RTTI 在 Java 中還有第三種形式,那就是關鍵字 instanceof,它返回一個布林值,告訴我們物件是不是某個特定型別的例項,可以用提問的方式使用它 ```java if(x instanceof Dog) { ((Dog)x).bark(); } ``` Java 還提供了 Class.isInstance() 方法動態檢測物件型別,例如 ```java 0 instance of String // 編譯報錯 String.class.isInstance(0) // 可以通過編譯 ```
## 反射 如果你不知道物件的確切型別,RTTI 會告訴你,但是有一個限制:必須在編譯時知道型別,才能使用 RTTI 檢測它。換句話說,編譯器必須知道你使用的所有類 看上去這並不是什麼特別大的限制,但假設你引用了一個不在程式空間中的物件,比如你從磁碟檔案或網路連線中獲得大量的位元組,並被告知這些位元組代表一個類,那該怎麼辦呢? 類 Class 支援反射的概念,java.lang.reflect 庫中支援類 Field、Method、Constructor(每一個都實現了 Member 介面),這些型別的物件由 JVM 執行時建立,以表示未知類中的對應成員。通常我們不會直接使用反射,但反射可以用來支援其他 Java 特性,例如物件序列化等 Field 代表類的成員變數(成員變數也稱為類的屬性),Class 類中定義瞭如下方法用來獲取 Field 物件 | 方法 | 用途 | | ----------------------------- | ---------------------- | | getField(String name) | 獲得某個公有的屬性物件 | | getFields() | 獲得所有公有的屬性物件 | | getDeclaredField(String name) | 獲得某個屬性物件 | | getDeclaredFields() | 獲得所有屬性物件 | Field 類定義瞭如下方法設定成員變數的資訊 | 方法 | 用途 | | ----------------------------- | -------------------------- | | equals(Object obj) | 屬性與 obj 相等則返回 true | | get(Object obj) | 獲得 obj 中對應的屬性值 | | set(Object obj, Object value) | 設定 obj 中對應屬性值 | Method 代表類的方法,Class 類中定義瞭如下方法用來獲取 Method 物件 | 方法 | 用途 | | ---------------------------------------------------------- | ---------------------- | | getMethod(String name, Class... parameterTypes) | 獲得該類某個公有的方法 | | getMethods() | 獲得該類所有公有的方法 | | getDeclaredMethod(String name, Class... parameterTypes) | 獲得該類某個方法 | | getDeclaredMethods() | 獲得該類所有方法 | Method 類定義瞭如下方法對方法進行呼叫 | 方法 | 用途 | | ---------------------------------- | ------------------------------------------ | | invoke(Object obj, Object... args) | 傳遞 object 物件及引數呼叫該物件對應的方法 | Constructor 代表類的構造器,Class 類中定義瞭如下方法用來獲取 Constructor 物件 | 方法 | 用途 | | -------------------------------------------------- | -------------------------------------- | | getConstructor(Class... parameterTypes) | 獲得該類中與引數型別匹配的公有構造方法 | | getConstructors() | 獲得該類的所有公有構造方法 | | getDeclaredConstructor(Class... parameterTypes) | 獲得該類中與引數型別匹配的構造方法 | | getDeclaredConstructors() | 獲得該類所有構造方法 | Constructor 代表類的構造方法 | 方法 | 用途 | | ------------------------------- | -------------------------- | | newInstance(Object... initargs) | 根據傳遞的引數建立類的物件 | 除了成員變數、方法和構造器以外,反射還能獲取其他更多的資訊,例如註解等,具體可查閱 Java API 反射的強大威力大家已經看到了,通過反射我們甚至可以獲取到一些“本不應該獲取”的資訊,例如程式設計師為了降低耦合,往往會使用介面來隔離元件,但反射卻可以輕易破解 ```java public interface A { void f(); } class B implements A { public void f() {} public void g() {} } public class InterfaceViolation { public static void main(String[] args) { A a = new B(); a.f(); // a.g(); // 編譯錯誤 if (a instanceof B) { B b = (B) a; b.g(); } } } ``` 通過使用 RTTI,我們發現 a 是用 B 實現的,只要將其轉型為 B,我們就可以呼叫不在 A 中的方法。如果你不希望客戶端開發者這樣做,那該如何解決呢?一種解決方案是直接宣告為實際型別,另一種則是讓實現類只具有包訪問許可權,這樣包外部的客戶端就看不到實現類了 除了這個以外,通過反射可以獲得所有成員資訊,包括 private 的,通常這種違反訪問許可權的操作並不是十惡不赦的,也許還可以幫助你解決某些特定型別的問題
## 動態代理 代理是基本的設計模式之一,一個物件封裝真實物件,代替真實物件提供其他不同的操作,這些操作通常涉及到與真實物件的通訊,因此代理通常充當中間物件。下面是一個簡單的靜態代理的示例: ```java interface Interface { void doSomething(); } class RealObject implements Interface { @Override public void doSomething() { System.out.println("doSomething"); } } class SimpleProxy implements Interface { private Interface proxied; SimpleProxy(Interface proxied) { this.proxied = proxied; } @Override public void doSomething() { System.out.println("SimpleProxy doSomething"); proxied.doSomething(); } } class SimpleProxyDemo { public static void consumer(Interface iface) { iface.doSomething(); } public static void main(String[] args) { consumer(new RealObject()); consumer(new SimpleProxy(new RealObject())); } } ``` 當你希望將額外的操作與真實物件做分離時,代理可能會有所幫助,而 Java 的動態代理更進一步,不僅動態建立代理物件,而且可以動態地處理對代理方法的呼叫。在動態代理上進行的所有呼叫都會重定向到一個**呼叫處理程式**,該程式負責發現呼叫的內容並決定如何處理,下面是一個簡單示例: ```java class DynamicProxyHandler implements InvocationHandler { private Object proxied; DynamicProxyHandler(Object proxied) { this.proxied = proxied; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { return method.invoke(proxied, args); } } class SimpleDynamicProxy { public static void consumer(Interface iface) { iface.doSomething(); } public static void main(String[] args) { RealObject real = new RealObject(); Interface proxy = (Interface) Proxy.newProxyInstance( Interface.class.getClassLoader(), new Class[]{Interface.class}, new DynamicProxyHandler(real)); consumer(proxy); } } ``` 通過呼叫靜態方法 Proxy.newProxyInstance() 來建立動態代理,該方法需要三個引數:類載入器、希望代理實現的介面列表、以及介面 InvocationHandler 的一個實現。InvocationHandler 正是我們所說的呼叫處理程式,動態代理的所有呼叫會被重定向到呼叫處理程式,因此通常為呼叫處理程式的建構函式提供一個真實物件的引用,以便執行中間操作後可以轉發請