摘要:接口與類型信息關(guān)鍵字的一種重要目標就是允許程序員隔離構(gòu)件,進而降低耦合性。如果你編寫接口,那么就可以實現(xiàn)這一目標,但是通過類型信息,這種耦合性還是會傳播出去接口并非是對解耦的一種無懈可擊的保障。
點擊進入我的博客
運行時類型信息使得你可以在運行時發(fā)現(xiàn)和使用類型信息,主要有兩種方式:
“傳統(tǒng)的”RTTI,它假定我們在編譯時已經(jīng)知道了所有的類型;
“反射”機制,它允許我們在運行時發(fā)現(xiàn)和使用類的信息。
14.1 為什么需要RTTIRTTI維護類型類型的信息,為多態(tài)機制的實現(xiàn)提供基礎。
14.2 Class對象類型信息在運行時是通過Class對象來表示的,完成的Class對象包含了與類有關(guān)的信息。Class對象就是用來創(chuàng)建所有“常規(guī)”對象的,Java使用Class對象來執(zhí)行RTTI
類是程序的一部分,每個類都有一個Class對象,被保存在一個同名的.class文件中。
類加載器子系統(tǒng)實際上可以包含一條類加載器鏈,但是只有一個原生類加載器,它是JVM實現(xiàn)的一部分。原生類加載器加載的是可信類,包括Java API類。
所有類都是在對其第一次使用(靜態(tài)成員或new對象)時,動態(tài)加載到JVM的。
Class對象僅在需要的時候才會加載,static初始化是在類加載時進行的。
類加載器首先會檢查這個類的Class對象是否已被加載過,如果尚未加載,默認的類加載器就會根據(jù)類名查找對應的.class文件。
想在運行時使用類型信息,必須獲取對象的Class對象的引用:Class.forName("s2.A");。該方法會自動初始化該Class對象,注意必須使用全限定名(包含包名)。
// 獲取類名 clz.getSimpleName() // 獲取全限定名 clz.getCanonicalName() // 獲取接口 clz.getInterfaces(); // 獲取父類 clz.getSuperClass(); // 創(chuàng)建該類對象 clz.newInstance();14.2.1 類字面常量
Java還提供了類字面常量的方式來生成對Class對象的引用:Class clz = A.class。注意這種方式不會自動初始化該Class對象。
類字面常量不僅可以用于普通的類,還可以用于接口、數(shù)組(int[].class)和基本數(shù)據(jù)類型(int.class)。
基本類型的包裝類,都有一個標準字段TYPE,這是一個指向?qū)緮?shù)據(jù)類型Class對象的引用:如public static final Class
加載:由類加載器完成,該步驟查找對應的字節(jié)碼,創(chuàng)建一個Class對象
鏈接:驗證類中的字節(jié)碼,為靜態(tài)域分配空間;并且如果必須的話,將解析這個類創(chuàng)建的對其他類的所有引用。
初始化:如果該類有超類,則對其初始化,執(zhí)行靜態(tài)初始化器和靜態(tài)初始化塊
使用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引用添加范型語法的原因僅僅是為了提供編譯期類型檢查。
cast()方法接受參數(shù)對象,將其轉(zhuǎn)型為Class引用的類型。
Class.asSubclass(),該方法允許你講一個類對象轉(zhuǎn)型為更加具體的類型。
Class14.3 類型轉(zhuǎn)換前先做檢查clz = String.class; String str1 = clz.cast("");
Java提供類instanceOf關(guān)鍵字,可以判斷對象是否是某個類(及其父類)的實例。
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 // true14.4 注冊工廠
使用工廠方法設計模式, 將對象的創(chuàng)建工作交給類自己去完成。 工廠方法可以被多態(tài)地調(diào)用, 從而為你創(chuàng)建恰當類型的對象。
14.5 instanceOf和Class的等價性instanceOf和isInstance()的結(jié)果完全一樣,比較的時候都考慮了繼承關(guān)系
A.class.equals(B.class) 和 A.class == B.class 只能比較是否為同一個類,沒有考慮繼承關(guān)系
14.6 反射:運行時的類信息如果不知道某個對象的確切類型,RTTI可以告訴你,但這有個限制:這個類型在編譯時必須已知。換句話說,編譯器在編譯時必須知道所有要通過RTTI來處理的類。
假設你獲取了一個指向某個并不在你程序空間中對象的引用,在編譯時你的程序根本無法獲知這個對象所屬的類。
運行時獲取類的信息場景:基于構(gòu)件的編程、遠程方法調(diào)用(RMI)。
當通過反射與一個未知類型的對象打交道時,JVM只是簡單地檢查這個對象,看它屬于哪個特定的類(就像RTTI那樣)。
在用它做其他事情之前必須先加載那個類的Class對象。因此,那個類的.class文件對于JVM來說必須是可獲取的:要么在本地機器上,要么可以通過網(wǎng)絡取得。
所以RTTI和反射之間真正的區(qū)別只在于,對RTTI來說,編譯器在編譯時打開和檢查.class文件;而對于反射機制來說,.class文件在編譯時是不可獲取的,所以是在運行時打開和檢查class文件。
反射在Java中是用來支持其他特性的,例如對象序列化和JavaBean。
14.7 動態(tài)代理代理是基本的設計模式之一,它是為你提供額外的或者不同的操作,而插入的用來代替“實際”對象的對象。這些操作通常設計與“實際”對象的通信,因此代理通常充當著中間人的角色。
靜態(tài)代理就是寫死了在代理對象中執(zhí)行這個方法前后執(zhí)行添加功能的形式。
優(yōu)點:可以做到在符合開閉原則的情況下對目標對象進行功能擴展。
缺點:我們得為每一個服務都得創(chuàng)建代理類,工作量太大,不易管理。同時接口一旦發(fā)生改變,代理類也得相應修改。
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的動態(tài)代理比代理的思想更向前邁進了一步, 因為它可以動態(tài)地創(chuàng)建代理并動態(tài)地處理對所代理方法的調(diào)用。在動態(tài)代理上所做的所有調(diào)用都會被重定向到單一的調(diào)用處理器上。
通過Proxy.newProxyInstance()可以創(chuàng)建動態(tài)代理,需要一個類加載器(通常是被加載的對象獲取)、一個希望實現(xiàn)的接口列表(不是類或抽象類)、以及InvocationHandler的一個實現(xiàn)。
動態(tài)代理可以將所有對接口的調(diào)用重定向為對代理的調(diào)用。
使用動態(tài)代理來編寫一個系統(tǒng)以實現(xiàn)事務,其中,代理在被代理的調(diào)用執(zhí)行成功(不拋出任何異常)執(zhí)行提交,而在執(zhí)行失敗時執(zhí)行回滾。你的提交和回滾都針對一個外部的文本文件,該文件不在Java異常的控制范圍之內(nèi)。你必須注意操作的原子性。
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沒有區(qū)別,但在很多地方就不必執(zhí)行額外的測試了,可以直接假設所有對象都是有效的。
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的子類,我們相對每一個都創(chuàng)建一個空對象。無論何時,如果你需要一個空Person對象,只需要調(diào)用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的一種更加智能化的替代物。
模擬對象和樁之間的差異在于程度不同。模擬對象往往是輕量級和自測試的,通常很多模擬對象被創(chuàng)建出來是為了處理各種不同的測試情況。樁只是返回樁數(shù)據(jù),它通常是重量級的,并且經(jīng)常在測試之間被復用。樁可以根據(jù)它們被調(diào)用的方式,通過配置進行修改,因此樁是一 種復雜對象,它要做很多事。然而對于模擬對象,如果你需要做很多事情,通常會創(chuàng)建大量小而簡單的模擬對象。
14.9 接口與類型信息interface關(guān)鍵字的一種重要目標就是允許程序員隔離構(gòu)件,進而降低耦合性。如果你編寫接口,那么就可以實現(xiàn)這一目標,但是通過類型信息,這種耦合性還是會傳播出去——接口并非是對解耦的一種無懈可擊的保障。
public class Test { public static void main(String[] args) { A a = new B(); a.a(); // 我們需要的是用戶使用接口,但是強制轉(zhuǎn)型還是可以訪問不存在于接口中的方法 ((B) a).b(); } } interface A { void a(); } class B implements A { @Override public void a() {} public void b() {} }
如果程序員不使用接口而是子類,它們要對自己負責。即B a = new B();代替A a = new B();。
此時在此包外只能使用Hidden.newA()來獲取對象,而且由于沒有B類的信息,也無法強制轉(zhuǎn)型。
class B implements A { @Override public void a() {} public void b() {} } public class HiddenB { public static A newA() { return new B(); } }
通過使用反射,仍舊可以到達并調(diào)用所有方法,甚至是private方法!如果知道方法名,你就可以在其Method對象上調(diào)用setAccessible(true)。
final域?qū)嶋H上在遭遇修改時是安全的。運行時系統(tǒng)會在不拋異常的情況下接受任何修改嘗試,但是實際上不會發(fā)生任何修改。
14.10 總結(jié)RTTI允許通過匿名基類的引用來發(fā)現(xiàn)類型信息。
面向?qū)ο缶幊陶Z言的目的是讓我們在凡是可以使用的地方都使用多態(tài)機制,只在必需的時候使用RTTI。
可繼承一個新類,然后添加你需要的方法。在代碼的其他地方,可以檢査你自己特定的類型,并調(diào)用你自己的方法,這樣做不會破壞多態(tài)性以及程序的擴展能力。
但如果在程序主體中添加需要的新特性的代碼,就必須使用RTTI來檢査你的特定的類型。
一致的錯誤報告模型的存在使我們能夠通過使用反射編寫動態(tài)代碼。當然,盡力編寫能夠進行靜態(tài)檢査的代碼是值得的,只要你確實能夠這么做。但是我相信動態(tài)代碼是將Java與其他例如C++這樣的語言區(qū)分開的重要工具之一。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/72201.html
摘要:自動拆箱用賦值運算符把一個包裝類賦值給一個基本類型變量,或者是在包裝類進行數(shù)值運算時。指數(shù)計數(shù),表示的冪按位操作符可以把值看成單比特值對待,的操作相同,但是不能用于布爾值。移位操作符高位包括符號位舍棄,低位補零。 點擊進入我的博客 3.1更簡單的打印語句 System.out.println(imbug); 通過編寫一個小類庫,并通過import static該方法來實現(xiàn)簡化打印(基...
摘要:方法的基本組成包括名稱參數(shù)返回值方法體方法名和參數(shù)列表唯一的標識出某個方法。如果返回的類型是,則的作用僅是退出方法否則必須返回正確的返回值包名名字可見性約定以域名反轉(zhuǎn)作為包名,用來劃分子目錄,并且全部小寫。 點擊進入我的博客 2.1用引用操縱對象 盡管一切都看作對象,但操縱的標識符實際上是對象的一個引用。 String s; // s是一個String類型的引用, 并沒有任何對象與其...
摘要:在初始化和步進控制部分,可以用一系列由逗號分割的語句,而且那些語句會獨立執(zhí)行。和都表示無限循環(huán)語法數(shù)組等關(guān)鍵詞有兩個方面的用途一方面指定一個方法返回什么值另一個方面指定當前的方法退出,并返回那個值。 點擊進入我的博客 4.1 true&false Java的條件語句只能使用布爾值來決定執(zhí)行路徑 4.2 if-else 4.3 循環(huán)語句 while、for、do-while do-...
摘要:一引用操縱對象在的世界里,一切都被視為對象。特點創(chuàng)建程序時,需要知道存儲在棧內(nèi)所有數(shù)據(jù)的確切生命周期,以便上下移動堆棧指針。因為,指向同一塊內(nèi)存空間除了通過對象引用靜態(tài)變量,我們還可以通過類直接引用靜態(tài)變量 一、引用操縱對象 在Java的世界里,一切都被視為對象。操縱的標識符實際上是對象的引用, 例如:遙控器與電視的關(guān)系。 可以在沒有對象關(guān)聯(lián)的情況下,擁有一個引用。沒有電視機,也可以擁...
摘要:多態(tài)的作用是消除類型之間的耦合關(guān)系。編寫構(gòu)造器準則用盡可能簡單的方法使對象進入正常狀態(tài),如果可以的話,避免調(diào)用其他方法。 點擊進入我的博客 在面向?qū)ο蟮某绦蛟O計語言中,多態(tài)是繼數(shù)據(jù)抽象(封裝)和繼承之后的第三種基本特征。多態(tài)通過分離做什么和怎么做,從另一角度將接口和實現(xiàn)分離開來。多態(tài)的作用是消除類型之間的耦合關(guān)系。 8.1 再論向上轉(zhuǎn)型 對象既可以作為它自己的本類使用,也可以作為它的...
閱讀 2025·2023-04-25 14:50
閱讀 2907·2021-11-17 09:33
閱讀 2611·2019-08-30 13:07
閱讀 2838·2019-08-29 16:57
閱讀 908·2019-08-29 15:26
閱讀 3540·2019-08-29 13:08
閱讀 1990·2019-08-29 12:32
閱讀 3383·2019-08-26 13:57