
什么是 RTTI?
RTTI 即 Runtime Type Information,顧名思義,也就是在運(yùn)行時,識別對象和類的信息。RTTI 有兩種,一種是“傳統(tǒng)的” RTTI,它假定我們在編譯時就已經(jīng)知道了所有的類型;另一種是“反射”機(jī)制,它允許我們在運(yùn)行時發(fā)現(xiàn)和使用類的信息。
最簡單的一個例子,比如:
List<Shape> shapeList = Arrays.asList(new Circle(), new Square(), new Triangle());
for(Shape shape : shapeList) {
shape.draw();
}
當(dāng)從 shapeList 中取出元素時(其實(shí)元素是用數(shù)組保存的,而數(shù)組會把所有元素都當(dāng)作 Object 來持有),會自動將元素轉(zhuǎn)型回對應(yīng)的對象。也就是說在保存對象和取出對象后,會發(fā)生以下這兩個過程:
(Object) shape // 向上轉(zhuǎn)型
(Shape) object // 向下轉(zhuǎn)型
這其實(shí)就是就是 RTTI 最基本的使用形式。在 Java 中,所有的類型轉(zhuǎn)換都是在運(yùn)行時進(jìn)行類型正確性檢查的。
Class 對象:類的信息
Class 對象包含了與類有關(guān)的信息,事實(shí)上 Java 中是用 Class 對象來創(chuàng)建這個類中的所有的對象的。每當(dāng)編譯完成,就會生成一個 Class 對象,被保存在 .class 文件中。JVM 使用 ClassLoader 來加載對象,所有的類都是在對其第一次使用(靜態(tài)成員被引用,靜態(tài)常量除外)或者用 new 關(guān)鍵字創(chuàng)建對象后,動態(tài)加載到 JVM 中的。所謂的動態(tài)加載也就是在被使用到時才去加載。
Class.forName("package.name.CanonicalName");
這個方法是獲取 Class 對象引用的一種方法,調(diào)用 Class.forName() 之后該類會被初始化。Class 對象中還有一個 newInstance() 的方法,可以用來創(chuàng)建對象新實(shí)例。除此之外,Class 對象中還有很多實(shí)用的方法,用來獲取類的信息,比如獲取類的接口、方法、成員變量等等。
類字面常量
我們還可以使用 .class 的形式來引用 Class 對象。
Class intClass = int.class;
泛化的 Class 引用
從 Java SE 5 開始,我們可以利用泛型對 Class 對象進(jìn)行類型限定。
Class<Integer> intClass = int.class; // legal
intClass = double.class; // illegal
instanceof
RTTI 除了可以確保類型轉(zhuǎn)換的正確性和通過 Class 對象獲取運(yùn)行時的類型信息外,還有第三種形式,那就是 instanceof,我們可以用這個關(guān)鍵字來確定某個對象是不是某個類的實(shí)例,比如:
Animal[] animals = {new Dog(), new Fish()};
for (Animal animal : animals) {
// 向下轉(zhuǎn)型前,先使用 instanceof 來判斷類型
if (animal instanceof Fish) {
Fish fish = (Fish) animal;
fish.swim();
}
}
反射:無所不能
RTTI 會在編譯期打開和檢查 .class 文件并利用這些信息做一些有用的事,而反射會在運(yùn)行時打開和檢查 .class 文件,這是 RTTI 和反射之間的真正區(qū)別。Java 中通過 Class 類和 java.lang.reflect 類庫對反射的概念進(jìn)行了支持。一起來看下這段代碼:
public static void main(String[] args) throws Exception {
// 在編譯期,Class.forName() 的結(jié)果是不可知的,只能通過反射去獲取運(yùn)行時的信息
Class<?> klass = Class.forName(args[0]);
Method[] methods = klass.getMethods();
for (Method method : methods) {
System.out.println(method);
}
}
以上 main() 方法中,通過讀取命令行參數(shù)實(shí)例化 Class 對象,然后打印該對象中的方法。此時的 klass 對象完全是未知的,但是我們可以通過反射去獲取其中的信息,創(chuàng)建對象或者調(diào)用方法。
反射是無孔不入的,無論是私有方法還是私有內(nèi)部類的方法,哪怕是匿名類的方法,也無法逃脫反射的調(diào)用。對于私有域來說也一樣,只有 final 域,才不會被修改。反射可以說是給我們的程序留了一道后門,但是總的來說,從反射給我們帶來的優(yōu)劣對比上看,利大于弊。
動態(tài)代理
代理是最常見的設(shè)計模式之一,它可以讓我們的代碼更加靈活,比如在進(jìn)行操作之前做一些額外的工作。下面是一個簡單代理的例子:
interface Interface {
void doSomething();
void doSomethingElse(String args);
}
class RealObject implements Interface {
@Override
public void doSomething() {
System.out.println("doSomething");
}
@Override
public void doSomethingElse(String args) {
System.out.println("doSomethingElse " + args);
}
}
class SimpleProxy implements Interface {
Interface mInterface;
public SimpleProxy(Interface anInterface) {
mInterface = anInterface;
}
@Override
public void doSomething() {
System.out.println("SimpleProxy doSomething");
mInterface.doSomething();
}
@Override
public void doSomethingElse(String args) {
System.out.println("SimpleProxy doSomethingElse");
mInterface.doSomethingElse(args);
}
}
簡單來說,代理就是將實(shí)際對象的方法調(diào)用分離開來,從而允許我們對一些操作進(jìn)行修改,或者添加額外的操作。而動態(tài)代理比代理的思想更進(jìn)一步,它允許我們動態(tài)地創(chuàng)建代理并動態(tài)地處理對所代理方法的調(diào)用。
public class SimpleDynamicProxy {
public static void consumer(Interface interf) {
interf.doSomething();
interf.doSomethingElse("args");
}
public static void main(String[] args) {
System.out.println("---------- no proxy ----------");
RealObject real = new RealObject();
consumer(real);
System.out.println("---------- simple proxy ----------");
consumer(new SimpleProxy(real));
System.out.println("---------- dynamic proxy ----------");
// 創(chuàng)建代理對象:設(shè)置 ClassLoader、接口 Class 對象、InvocationHandler
Interface proxy = (Interface) Proxy.newProxyInstance(
Interface.class.getClassLoader(),
new Class[]{Interface.class}, // 數(shù)組里的 Class 對象必須是接口且不能重復(fù)
new DynamicProxyHandler(real));
consumer(proxy);
}
/**
* 代理對象必須實(shí)現(xiàn)自己的 InvocationHandler,所有的調(diào)用都會被重定向到這個調(diào)用處理器上
*/
static class DynamicProxyHandler implements InvocationHandler {
/**
* 被代理的對象,調(diào)用的請求會轉(zhuǎn)發(fā)到這個“實(shí)際”對象上
*/
private Object proxied;
public DynamicProxyHandler(Object proxied) {
this.proxied = proxied;
}
/**
* 該方法接收三個參數(shù),代理對象的實(shí)例、調(diào)用的方法的實(shí)例以及方法的參數(shù)。
* 我們一般在這里確定調(diào)用該方法時所采取的措施。
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("Proxy: " + proxy.getClass()
+ ", method: " + method + ", args: " + args);
// 如果方法參數(shù)不為空則輸出參數(shù)
if (args != null) {
for (int i = 0; i < args.length; i++) {
System.out.println("args[" + i + "] = " + args[i]);
}
}
// 轉(zhuǎn)發(fā)請求給被代理對象
return method.invoke(proxied, args);
}
}
}
使用動態(tài)代理其實(shí)很簡單,首先要定義一個自己的 InvocationHandler,然后再通過 Proxy.newProxyInstance 創(chuàng)建一個代理對象。
使用動態(tài)代理的優(yōu)勢
動態(tài)代理的優(yōu)點(diǎn)主要有兩個:
- 更強(qiáng)的靈活性。我們不用在設(shè)計實(shí)現(xiàn)的時候就指定某一個代理類來代理某一個被代理對象,而是可以把這種指定延遲到程序運(yùn)行時由 JVM 來實(shí)現(xiàn)。
- 動態(tài)代理更為統(tǒng)一與簡潔。
第一點(diǎn)從上面的例子就能看出,我們必須事先就確定 SimpleProxy 作為 Interface 的代理,并且編寫每種方法對應(yīng)的代碼,而動態(tài)代理允許我們利用反射機(jī)制生成任意類型的動態(tài)代理類。第二點(diǎn)也很明顯,當(dāng)使用動態(tài)代理的時候,我們只需要在一個地方進(jìn)行修改,寫的代碼也變少了。
參考資料:
- Think in Java(第4版)
- Java 動態(tài)代理(Dynamic proxy) 小結(jié)
- 代理模式原理及實(shí)例講解