《Java編程思想》筆記14.類型信息
運行時類型信息使得你可以在運行時發現和使用類型信息,主要有兩種方式:
- “傳統的”RTTI,它假定我們在編譯時已經知道了所有的類型;
- “反射”機制,它允許我們在運行時發現和使用類的信息。
14.1 為什麽需要RTTI
RTTI維護類型類型的信息,為多態機制的實現提供基礎。
14.2 Class對象
類型信息在運行時是通過Class
對象來表示的,完成的Class
對象包含了與類有關的信息。Class
對象就是用來創建所有“常規”對象的,Java使用Class
對象來執行RTTI
類是程序的一部分,每個類都有一個Class
對象,被保存在一個同名的.class
文件中。
類加載器
- 類加載器子系統實際上可以包含一條類加載器鏈,但是只有一個原生類加載器
- 所有類都是在對其第一次使用(靜態成員或new對象)時,動態加載到JVM的。
-
Class
對象僅在需要的時候才會加載,static
初始化是在類加載時進行的。 - 類加載器首先會檢查這個類的
Class
對象是否已被加載過,如果尚未加載,默認的類加載器就會根據類名查找對應的.class
文件。
Class類方法
想在運行時使用類型信息,必須獲取對象的Class對象的引用:Class.forName("s2.A");
。該方法會自動初始化該Class對象,註意必須使用全限定名(包含包名)。
// 獲取類名 clz.getSimpleName() // 獲取全限定名 clz.getCanonicalName() // 獲取接口 clz.getInterfaces(); // 獲取父類 clz.getSuperClass(); // 創建該類對象 clz.newInstance();
14.2.1 類字面常量
Java還提供了類字面常量的方式來生成對Class對象的引用:Class clz = A.class
。註意這種方式不會自動初始化該Class對象。
基本類型
- 類字面常量不僅可以用於普通的類,還可以用於接口、數組(int[].class)和基本數據類型(int.class)。
- 基本類型的包裝類,都有一個標準字段TYPE,這是一個指向對應基本數據類型Class對象的引用:如
public static final Class<Integer> TYPE = (Class<Integer>) Class.getPrimitiveClass("int");
類的準備過程
- 加載:由類加載器完成,該步驟查找對應的字節碼,創建一個Class對象
- 鏈接:驗證類中的字節碼,為靜態域分配空間;並且如果必須的話,將解析這個類創建的對其他類的所有引用。
- 初始化:如果該類有超類,則對其初始化,執行靜態初始化器和靜態初始化塊
初始化的惰性
- 使用
Class.forName()
會自動初始化;使用A.class
不會自動初始化類 - 編譯期常量:
static final int i = 1;
的值,則不需要初始化就可以被讀取。 - 如果只是將一個域設置為
static final
不足以保證是編譯器常量,如static final int ii = new Random().nextInt();
。 - 如果一個
static
域不是final
的,那麽訪問之前要先進行鏈接和初始化。
14.2.2 泛化的Class引用
Java SE5之後,Class也可以支持範型了。
向Class引用添加範型語法的原因僅僅是為了提供編譯期類型檢查。
14.2.3 新的轉型語法
- cast()方法接受參數對象,將其轉型為Class引用的類型。
- Class.asSubclass(),該方法允許你講一個類對象轉型為更加具體的類型。
Class<String> clz = String.class;
String str1 = clz.cast("");
14.3 類型轉換前先做檢查
- Java提供類
instanceOf
關鍵字,可以判斷對象是否是某個類(及其父類)的實例。 -
clz.isInstance()
方法接受一個對象,判斷該對象是否是該clz
指向的類的實例。 -
clz.isAssignableFrom()
方法接受一個Class
對象,判斷該Class
對象是否是clz
自身或子類。
public class Test {
public static void main(String[] args) throws Exception {
System.out.println(A.class.isAssignableFrom(C.class));
System.out.println(B.class.isAssignableFrom(C.class));
System.out.println(A.class.isInstance(new C()));
System.out.println(B.class.isInstance(new C()));
}
}
class A { }
interface B {}
class C extends A implements B {}
// Output:
// true
// true
// true
// true
14.4 註冊工廠
使用工廠方法設計模式, 將對象的創建工作交給類自己去完成。 工廠方法可以被多態地調用, 從而為你創建恰當類型的對象。
14.5 instanceOf和Class的等價性
- instanceOf和isInstance()的結果完全一樣,比較的時候都考慮了繼承關系
- A.class.equals(B.class) 和 A.class == B.class 只能比較是否為同一個類,沒有考慮繼承關系
14.6 反射:運行時的類信息
RTTI的限制
- 如果不知道某個對象的確切類型,RTTI可以告訴你,但這有個限制:這個類型在編譯時必須已知。換句話說,編譯器在編譯時必須知道所有要通過RTTI來處理的類。
- 假設你獲取了一個指向某個並不在你程序空間中對象的引用,在編譯時你的程序根本無法獲知這個對象所屬的類。
- 運行時獲取類的信息場景:基於構件的編程、遠程方法調用(RMI)。
反射機制並沒有什麽神奇之處
- 當通過反射與一個未知類型的對象打交道時,JVM只是簡單地檢查這個對象,看它屬於哪個特定的類(就像RTTI那樣)。
- 在用它做其他事情之前必須先加載那個類的Class對象。因此,那個類的.class文件對於JVM來說必須是可獲取的:要麽在本地機器上,要麽可以通過網絡取得。
- 所以RTTI和反射之間真正的區別只在於,對RTTI來說,編譯器在編譯時打開和檢查.class文件;而對於反射機制來說,.class文件在編譯時是不可獲取的,所以是在運行時打開和檢查class文件。
反射的作用
反射在Java中是用來支持其他特性的,例如對象序列化和JavaBean。
14.7 動態代理
代理是基本的設計模式之一,它是為你提供額外的或者不同的操作,而插入的用來代替“實際”對象的對象。這些操作通常設計與“實際”對象的通信,因此代理通常充當著中間人的角色。
靜態代理
- 靜態代理就是寫死了在代理對象中執行這個方法前後執行添加功能的形式。
- 優點:可以做到在符合開閉原則的情況下對目標對象進行功能擴展。
- 缺點:我們得為每一個服務都得創建代理類,工作量太大,不易管理。同時接口一旦發生改變,代理類也得相應修改。
public class Test {
public static void main(String[] args) throws Exception {
new RealObject().doSomething();
System.out.println("代理之後:");
new SimpleProxy(new RealObject()).doSomething();
}
}
interface MyInterface {
void doSomething();
}
class RealObject implements MyInterface {
@Override
public void doSomething() {
System.out.println("RealObject");
}
}
class SimpleProxy implements MyInterface {
private MyInterface myInterface;
public SimpleProxy(MyInterface myInterface) {
this.myInterface = myInterface;
}
// 代理後增加方法
@Override
public void doSomething() {
System.out.println("SimpleProxy");
myInterface.doSomething();
}
}
動態代理
- Java的動態代理比代理的思想更向前邁進了一步, 因為它可以動態地創建代理並動態地處理對所代理方法的調用。在動態代理上所做的所有調用都會被重定向到單一的調用處理器上。
- 通過Proxy.newProxyInstance()可以創建動態代理,需要一個類加載器(通常是被加載的對象獲取)、一個希望實現的接口列表(不是類或抽象類)、以及InvocationHandler的一個實現。
- 動態代理可以將所有對接口的調用重定向為對代理的調用。
- 使用動態代理來編寫一個系統以實現事務,其中,代理在被代理的調用執行成功(不拋出任何異常)執行提交,而在執行失敗時執行回滾。你的提交和回滾都針對一個外部的文本文件,該文件不在Java異常的控制範圍之內。你必須註意操作的原子性。
MyInterface myInterface = (MyInterface) Proxy.newProxyInstance(MyInterface.class.getClassLoader(), new Class[]{MyInterface.class}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("代理方法");
return method.invoke(new RealObject(), args);
}
});
myInterface.doSomething();
14.8 空對象
- 使用
null
的時候每次都要檢查是否為null
,這是一件很麻煩的事。 - 引人空對象的思想將會很有用,它可以接受傳遞給它的所代表的對象的消息,但是將返回表示為實際上並不存在任何“真實”對象的值。通過這種方式,你可以假設所有的對象都是有效的,而不必浪費編程精力去檢查
null
。 - 通常空對象是單例的,所以你不僅可以用
instanceOf
來比較,還可以用equals
或==
來比較。 - 註意:在某些地方仍然必須測試空對象,這與檢查是否為
null
沒有區別,但在很多地方就不必執行額外的測試了,可以直接假設所有對象都是有效的。
public class Test {
public static void main(String[] args) throws Exception {
// 在使用的時候可以直接使用而不會報錯空指針
Person p = Person.NULL_PERSON;
System.out.println(p.toString());
}
}
// 空標記接口
interface Null {}
class Person {
void func() {
System.out.println("Person");
}
// 空對象
private static class NullPerson extends Person implements Null {
private NullPerson() {}
@Override
public String toString() {
return "NullPerson";
}
}
public static final Person NULL_PERSON = new NullPerson();
}
動態代理創建空對象
假設有不同的多個Person
的子類,我們相對每一個都創建一個空對象。無論何時,如果你需要一個空Person
對象,只需要調用newNullPerson()
並傳遞需要代理的Person
的類型。
public class Test {
public static Person newNullPerson(Class<? extends Person> type) {
return (Person) Proxy.newProxyInstance(NullPerson.NULL_PERSON.getClass().getClassLoader(), new Class[]{type}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("代理");
return method.invoke(NullPerson.NULL_PERSON, args);
}
});
}
public static void main(String[] args) throws Exception {
Person p = newNullPerson(Person.class);
p.func();
}
}
// 空標記接口
interface Null {}
// 父接口
interface Person {
void func();
}
// 空Person
class NullPerson implements Person, Null {
@Override
public void func() {
System.out.println("NullPerson");
}
public static final Person NULL_PERSON = new NullPerson();
private NullPerson() {}
}
14.8.1 模擬對象與樁
空對象的邏輯變體是模擬對象和樁。與空對象一樣,它們都表示在最終的程序中所使用的“實際”對象。但是,模擬對象和樁都只是假扮可以傳遞實際信息的存活對象,而不是像空對象那樣可以成為null
的一種更加智能化的替代物。
模擬對象和樁之間的差異在於程度不同。模擬對象往往是輕量級和自測試的,通常很多模擬對象被創建出來是為了處理各種不同的測試情況。樁只是返回樁數據,它通常是重量級的,並且經常在測試之間被復用。樁可以根據它們被調用的方式,通過配置進行修改,因此樁是一 種復雜對象,它要做很多事。然而對於模擬對象,如果你需要做很多事情,通常會創建大量小而簡單的模擬對象。
14.9 接口與類型信息
interface關鍵字的一種重要目標就是允許程序員隔離構件,進而降低耦合性。如果你編寫接口,那麽就可以實現這一目標,但是通過類型信息,這種耦合性還是會傳播出去——接口並非是對解耦的一種無懈可擊的保障。
public class Test {
public static void main(String[] args) {
A a = new B();
a.a();
// 我們需要的是用戶使用接口,但是強制轉型還是可以訪問不存在於接口中的方法
((B) a).b();
}
}
interface A {
void a();
}
class B implements A {
@Override
public void a() {}
public void b() {}
}
解決方法1:方法是直接聲明
如果程序員不使用接口而是子類,它們要對自己負責。即B a = new B();
代替A a = new B();
。
解決方法2:包訪問權限隱藏
此時在此包外只能使用Hidden.newA()來獲取對象,而且由於沒有B類的信息,也無法強制轉型。
class B implements A {
@Override
public void a() {}
public void b() {}
}
public class HiddenB {
public static A newA() {
return new B();
}
}
反射的後門
- 通過使用反射,仍舊可以到達並調用所有方法,甚至是
private
方法!如果知道方法名,你就可以在其Method
對象上調用setAccessible(true)
。 -
final
域實際上在遭遇修改時是安全的。運行時系統會在不拋異常的情況下接受任何修改嘗試,但是實際上不會發生任何修改。
14.10 總結
- RTTI允許通過匿名基類的引用來發現類型信息。
- 面向對象編程語言的目的是讓我們在凡是可以使用的地方都使用多態機制,只在必需的時候使用RTTI。
- 可繼承一個新類,然後添加你需要的方法。在代碼的其他地方,可以檢査你自己特定的類型,並調用你自己的方法,這樣做不會破壞多態性以及程序的擴展能力。
- 但如果在程序主體中添加需要的新特性的代碼,就必須使用RTTI來檢査你的特定的類型。
- 一致的錯誤報告模型的存在使我們能夠通過使用反射編寫動態代碼。當然,盡力編寫能夠進行靜態檢査的代碼是值得的,只要你確實能夠這麽做。但是我相信動態代碼是將Java與其他例如C++這樣的語言區分開的重要工具之一。
原文地址:https://segmentfault.com/a/1190000017035498
《Java編程思想》筆記14.類型信息