本文部分摘自 On Java 8
RTTI
RTTI(RunTime Type Information)運行時類型信息,能夠在程序運行時發現和使用類型信息,把我們從只能在編譯期知曉類型信息並操作的局限中解脫出來
傳統的多態機制正是 RTTI 的基本使用:假設有一個基類 Shape 和它的三個子類 Circle、Square、Triangle,現在要把 Circle、Square、Triangle 對象放入 List<Shape> 中,在運行時,先把放入其中的所有對象都當作 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
Class gumClass = Class.forName("Gum");
使用 Class.forName() 你不需要先持有這個類型的對象,但如果你已經擁有了目標類的對象,那就可以通過調用 getClass() 方法來獲取 Class 引用,這個方法來自根類 Object,它將返回表示該對象實際類型的 Class 對象的引用
Gum gum = new Gum();
Class gumClass = gum.getClass();
另外,你還可以調用 getSuperclass() 方法來得到父類的 class 對象,再用父類的 Class 對象調用該方法,重復多次,你就可以得到一個完整的類繼承結構
Class 對象的 newInstance() 方法可以讓你在不知道一個的確切類型的時候創建這個類的對象,使用 newInstance() 來創建的類,必須帶有無參數的構造器
Object obj = gumClass.newInstance();
當然,由於得到的是 Object 的引用,目前你只能給它發送 Object 對象能接受的調用。如果你想請求具體對象才有的調用,你就得先獲取該對象的更多類型信息,並執行轉型
Java 還提供了另一種生成類對象的引用:類字面常量,這樣做不僅更簡單,而且更安全,因為它在編譯時就會收到檢查(不用放在 try 語句塊中),而且根除了對 forName() 方法的調用,效率更高
Class gumClass = Gum.class;
類字面常量不僅可以用於普通類,也可以用於接口、數組以及基本數據類型。對於基本數據類型的包裝類,還有一個標准字段 Type,Type 字段是一個引用,指向對應基本數據類型的 Class 對象,例如 int.class 就等價於 Integer.TYPE。還有一點值得注意的是:使用 .class 語法來獲得對類對象的引用不會觸發初始化
到這里我們都知道了,Class 引用總是指向某個 Class 對象,而 Class 對象可以用於產生類的實例。不過自從 Java 引入泛型以后,我們就可以使用泛型對 Class 引用所指向的 Class 對象的類型進行限定,讓它的類型變得更具體些
Class intClass = int.class;
Class<Integer> 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 類型包括:
- 傳統的類型轉換,如多態
- 代表對象類型的 Class 對象
RTTI 在 Java 中還有第三種形式,那就是關鍵字 instanceof,它返回一個布爾值,告訴我們對象是不是某個特定類型的實例,可以用提問的方式使用它
if(x instanceof Dog) {
((Dog)x).bark();
}
Java 還提供了 Class.isInstance() 方法動態檢測對象類型,例如
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
反射的強大威力大家已經看到了,通過反射我們甚至可以獲取到一些“本不應該獲取”的信息,例如程序員為了降低耦合,往往會使用接口來隔離組件,但反射卻可以輕易破解
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 的,通常這種違反訪問權限的操作並不是十惡不赦的,也許還可以幫助你解決某些特定類型的問題
動態代理
代理是基本的設計模式之一,一個對象封裝真實對象,代替真實對象提供其他不同的操作,這些操作通常涉及到與真實對象的通信,因此代理通常充當中間對象。下面是一個簡單的靜態代理的示例:
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 的動態代理更進一步,不僅動態創建代理對象,而且可以動態地處理對代理方法的調用。在動態代理上進行的所有調用都會重定向到一個調用處理程序,該程序負責發現調用的內容並決定如何處理,下面是一個簡單示例:
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 正是我們所說的調用處理程序,動態代理的所有調用會被重定向到調用處理程序,因此通常為調用處理程序的構造函數提供一個真實對象的引用,以便執行中間操作后可以轉發請求