《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