點擊上方"java全棧技術"關注,每天學習一個java知識點
本篇主要是深入對Java中的Class對象進行分析,這對後續深入理解反射技術非常重要,主要內容如下:
- 深入理解Class對象
- RRTI的概念以及Class對象作用
- Class對象的加載及其獲取方式
- Class對象的加載
- ClassforName方法
- Class字面常量
- 理解泛化的Class對象引用
- 關於類型轉換的問題
- instanceof 關鍵字與isInstance方法
- 理解反射技術
- Constructor類及其用法
- Field類及其用法
- Method類及其用法
- 反射包中的Array類
深入理解Class對象
RRTI的概念以及Class對象作用
認識Class對象之前,先來了解一個概念,RTTI(Run-Time Type Identification)運行時類型識別,對於這個詞一直是 C++ 中的概念,至於Java中出現RRTI的說法則是源於《Thinking in Java》一書,其作用是在運行時識別一個對象的類型和類的信息,這裡分兩種:傳統的”RRTI”,它假定我們在編譯期已知道了所有類型(在沒有反射機制創建和使用類對象時,一般都是編譯期已確定其類型,如new對象時該類必須已定義好),另外一種是反射機制,它允許我們在運行時發現和使用類型的信息。在Java中用來表示運行時類型信息的對應類就是Class類,Class類也是一個實實在在的類,存在於JDK的java.lang包中,其部分源碼如下:
Class類被創建後的對象就是Class對象,注意,Class對象表示的是自己手動編寫類的類型信息,比如創建一個Shapes類,那麼,JVM就會創建一個Shapes對應Class類的Class對象,該Class對象保存了Shapes類相關的類型信息。實際上在Java中每個類都有一個Class對象,每當我們編寫並且編譯一個新創建的類就會產生一個對應Class對象並且這個Class對象會被保存在同名.class文件裡(編譯後的字節碼文件保存的就是Class對象),那為什麼需要這樣一個Class對象呢?是這樣的,當我們new一個新對象或者引用靜態成員變量時,Java虛擬機(JVM)中的類加載器子系統會將對應Class對象加載到JVM中,然後JVM再根據這個類型信息相關的Class對象創建我們需要實例對象或者提供靜態變量的引用值。需要特別注意的是,手動編寫的每個class類,無論創建多少個實例對象,在JVM中都只有一個Class對象,即在內存中每個類有且只有一個相對應的Class對象,挺拗口,通過下圖理解(內存中的簡易現象圖):
到這我們也就可以得出以下幾點信息:
- Class類也是類的一種,與class關鍵字是不一樣的。
- 手動編寫的類被編譯後會產生一個Class對象,其表示的是創建的類的類型信息,而且這個Class對象保存在同名.class的文件中(字節碼文件),比如創建一個Shapes類,編譯Shapes類後就會創建其包含Shapes類相關類型信息的Class對象,並保存在Shapes.class字節碼文件中。
- 每個通過關鍵字class標識的類,在內存中有且只有一個與之對應的Class對象來描述其類型信息,無論創建多少個實例對象,其依據的都是用一個Class對象。
- Class類只存私有構造函數,因此對應Class對象只能有JVM創建和加載
- Class類的對象作用是運行時提供或獲得某個對象的類型信息,這點對於反射技術很重要(關於反射稍後分析)。
Class對象的加載及其獲取方式
Class對象的加載
前面我們已提到過,Class對象是由JVM加載的,那麼其加載時機是?實際上所有的類都是在對其第一次使用時動態加載到JVM中的,當程序創建第一個對類的靜態成員引用時,就會加載這個被使用的類(實際上加載的就是這個類的字節碼文件),注意,使用new操作符創建類的新實例對象也會被當作對類的靜態成員的引用(構造函數也是類的靜態方法),由此看來Java程序在它們開始運行之前並非被完全加載到內存的,其各個部分是按需加載,所以在使用該類時,類加載器首先會檢查這個類的Class對象是否已被加載(類的實例對象創建時依據Class對象中類型信息完成的),如果還沒有加載,默認的類加載器就會先根據類名查找.class文件(編譯後Class對象被保存在同名的.class文件中),在這個類的字節碼文件被加載時,它們必須接受相關驗證,以確保其沒有被破壞並且不包含不良Java代碼(這是java的安全機制檢測),完全沒有問題後就會被動態加載到內存中,此時相當於Class對象也就被載入內存了(畢竟.class字節碼文件保存的就是Class對象),同時也就可以被用來創建這個類的所有實例對象。下面通過一個簡單例子來說明Class對象被加載的時機問題(例子引用自Thinking in Java):
在上述代碼中,每個類Candy、Gum、Cookie都存在一個static語句,這個語句會在類第一次被加載時執行,這個語句的作用就是告訴我們該類在什麼時候被加載,執行結果:
inside main
Loading Candy
After creating Candy
Loading Gum
After Class.forName("com.zejian.Gum")
Loading Cookie
After creating Cookie
Process finished with exit code 0
從結果來看,new一個Candy對象和Cookie對象,構造函數將被調用,屬於靜態方法的引用,Candy類的Class對象和Cookie的Class對象肯定會被加載,畢竟Candy實例對象的創建依據其Class對象。比較有意思的是
Class.forName("com.zejian.Gum");
其中forName方法是Class類的一個static成員方法,記住所有的Class對象都源於這個Class類,因此Class類中定義的方法將適應所有Class對象。這裡通過forName方法,我們可以獲取到Gum類對應的Class對象引用。從打印結果來看,調用forName方法將會導致Gum類被加載(前提是Gum類從來沒有被加載過)。
Class.forName方法
通過上述的案例,我們也就知道Class.forName()方法的調用將會返回一個對應類的Class對象,因此如果我們想獲取一個類的運行時類型信息並加以使用時,可以調用Class.forName()方法獲取Class對象的引用,這樣做的好處是無需通過持有該類的實例對象引用而去獲取Class對象,如下的第2種方式是通過一個實例對象獲取一個類的Class對象,其中的getClass()是從頂級類Object繼承而來的,它將返回表示該對象的實際類型的Class對象引用。
注意調用forName方法時需要捕獲一個名稱為ClassNotFoundException的異常,因為forName方法在編譯器是無法檢測到其傳遞的字符串對應的類是否存在的,只能在程序運行時進行檢查,如果不存在就會拋出ClassNotFoundException異常。
Class字面常量
在Java中存在另一種方式來生成Class對象的引用,它就是Class字面常量,如下:
//字面常量的方式獲取Class對象Class clazz = Gum.class;
這種方式相對前面兩種方法更加簡單,更安全。因為它在編譯器就會受到編譯器的檢查同時由於無需調用forName方法效率也會更高,因為通過字面量的方法獲取Class對象的引用不會自動初始化該類。更加有趣的是字面常量的獲取Class對象引用方式不僅可以應用於普通的類,也可以應用用接口,數組以及基本數據類型,這點在反射技術應用傳遞參數時很有幫助,關於反射技術稍後會分析,由於基本數據類型還有對應的基本包裝類型,其包裝類型有一個標準字段TYPE,而這個TYPE就是一個引用,指向基本數據類型的Class對象,其等價轉換如下,一般情況下更傾向使用.class的形式,這樣可以保持與普通類的形式統一。
boolean.class = Boolean.TYPE;char.class = Character.TYPE;byte.class = Byte.TYPE;short.class = Short.TYPE;int.class = Integer.TYPE;long.class = Long.TYPE;float.class = Float.TYPE;double.class = Double.TYPE;void.class = Void.TYPE;
前面提到過,使用字面常量的方式獲取Class對象的引用不會觸發類的初始化,這裡我們可能需要簡單瞭解一下類加載的過程,如下:
- 加載:類加載過程的一個階段:通過一個類的完全限定查找此類字節碼文件,並利用字節碼文件創建一個Class對象
- 鏈接:驗證字節碼的安全性和完整性,準備階段正式為靜態域分配存儲空間,注意此時只是分配靜態成員變量的存儲空間,不包含實例成員變量,如果必要的話,解析這個類創建的對其他類的所有引用。
- 初始化:類加載最後階段,若該類具有超類,則對其進行初始化,執行靜態初始化器和靜態初始化成員變量。
由此可知,我們獲取字面常量的Class引用時,觸發的應該是加載階段,因為在這個階段Class對象已創建完成,獲取其引用並不困難,而無需觸發類的最後階段初始化。下面通過小例子來驗證這個過程:
執行結果:
After creating Initable ref47Initializing Initable258Initializing Initable2147Initializing Initable3After creating Initable3 ref74
從輸出結果來看,可以發現,通過字面常量獲取方式獲取Initable類的Class對象並沒有觸發Initable類的初始化,這點也驗證了前面的分析,同時發現調用Initable.staticFinal變量時也沒有觸發初始化,這是因為staticFinal屬於編譯期靜態常量,在編譯階段通過常量傳播優化的方式將Initable類的常量staticFinal存儲到了一個稱為NotInitialization類的常量池中,在以後對Initable類常量staticFinal的引用實際都轉化為對NotInitialization類對自身常量池的引用,所以在編譯期後,對編譯期常量的引用都將在NotInitialization類的常量池獲取,這也就是引用編譯期靜態常量不會觸發Initable類初始化的重要原因。但在之後調用了Initable.staticFinal2變量後就觸發了Initable類的初始化,注意staticFinal2雖然被static和final修飾,但其值在編譯期並不能確定,因此staticFinal2並不是編譯期常量,使用該變量必須先初始化Initable類。Initable2和Initable3類中都是靜態成員變量並非編譯期常量,引用都會觸發初始化。至於forName方法獲取Class對象,肯定會觸發初始化,這點在前面已分析過。到這幾種獲取Class對象的方式也都分析完,ok~,到此這裡可以得出小結論:
- 獲取Class對象引用的方式3種,通過繼承自Object類的getClass方法,Class類的靜態方法forName以及字面常量的方式”.class”。
- 其中實例類的getClass方法和Class類的靜態方法forName都將會觸發類的初始化階段,而字面常量獲取Class對象的方式則不會觸發初始化。
- 初始化是類加載的最後一個階段,也就是說完成這個階段後類也就加載到內存中(Class對象在加載階段已被創建),此時可以對類進行各種必要的操作了(如new對象,調用靜態成員等),注意在這個階段,才真正開始執行類中定義的Java程序代碼或者字節碼。
關於類加載的初始化階段,在虛擬機規範嚴格規定了有且只有5種場景必須對類進行初始化:
- 使用new關鍵字實例化對象時、讀取或者設置一個類的靜態字段(不包含編譯期常量)以及調用靜態方法的時候,必須觸發類加載的初始化過程(類加載過程最終階段)。
- 使用反射包(java.lang.reflect)的方法對類進行反射調用時,如果類還沒有被初始化,則需先進行初始化,這點對反射很重要。
- 當初始化一個類的時候,如果其父類還沒進行初始化則需先觸發其父類的初始化。
- 當Java虛擬機啟動時,用戶需要指定一個要執行的主類(包含main方法的類),虛擬機會先初始化這個主類
- 當使用JDK 1.7 的動態語言支持時,如果一個java.lang.invoke.MethodHandle 實例最後解析結果為REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,並且這個方法句柄對應類沒有初始化時,必須觸發其初始化(這點看不懂就算了,這是1.7的新增的動態語言支持,其關鍵特徵是它的類型檢查的主體過程是在運行期而不是編譯期進行的,這是一個比較大點的話題,這裡暫且打住)
理解泛化的Class對象引用
由於Class的引用總數指向某個類的Class對象,利用Class對象可以創建實例類,這也就足以說明Class對象的引用指向的對象確切的類型。在Java SE5引入泛型後,使用我們可以利用泛型來表示Class對象更具體的類型,即使在運行期間會被擦除,但編譯期足以確保我們使用正確的對象類型。如下:
//編譯無法通過ClassnumberClass=Integer.class;
Class> intClass = int.class;intClass = double.class;
這樣的語句並沒有什麼問題,畢竟通配符指明所有類型都適用,那麼為什麼不直接使用Class還要使用Class>呢?這樣做的好處是告訴編譯器,我們是確實是採用任意類型的泛型,而非忘記使用泛型約束,因此Class>總是優於直接使用Class,至少前者在編譯器檢查時不會產生警告信息。當然我們還可以使用extends關鍵字告訴編譯器接收某個類型的子類,如解決前面Number與Integer的問題:
//編譯通過!Class extends Number> clazz = Integer.class;//賦予其他類型clazz = double.class;clazz = Number.class;
上述的代碼是行得通的,extends關鍵字的作用是告訴編譯器,只要是Number的子類都可以賦值。這點與前面直接使用Class
關於類型轉換的問題
在許多需要強制類型轉換的場景,我們更多的做法是直接強制轉換類型:
之所可以強制轉換,這得歸功於RRTI,要知道在Java中,所有類型轉換都是在運行時進行正確性檢查的,利用RRTI進行判斷類型是否正確從而確保強制轉換的完成,如果類型轉換失敗,將會拋出類型轉換異常。除了強制轉換外,在Java SE5中新增一種使用Class對象進行類型轉換的方式,如下:
Animal animal= new Dog();//這兩句等同於Dog dog = (Dog) animal;ClassdogType = Dog.class;Dog dog = dogType.cast(animal)
利用Class對象的cast方法,其參數接收一個參數對象並將其轉換為Class引用的類型。這種方式似乎比之前的強制轉換更麻煩些,確實如此,而且當類型不能正確轉換時,仍然會拋出ClassCastException異常。源碼如下:
instanceof 關鍵字與isInstance方法
關於instanceof 關鍵字,它返回一個boolean類型的值,意在告訴我們對象是不是某個特定的類型實例。如下,在強制轉換前利用instanceof檢測obj是不是Animal類型的實例對象,如果返回true再進行類型轉換,這樣可以避免拋出類型轉換的異常(ClassCastException)
而isInstance方法則是Class類中的一個Native方法,也是用於判斷對象類型的,看個簡單例子:
事實上instanceOf 與isInstance方法產生的結果是相同的。對於instanceOf是關鍵字只被用於對象引用變量,檢查左邊對象是不是右邊類或接口的實例化。如果被測對象是null值,則測試結果總是false。一般形式:
//判斷這個對象是不是這種類型obj.instanceof(class)
而isInstance方法則是Class類的Native方法,其中obj是被測試的對象或者變量,如果obj是調用這個方法的class或接口的實例,則返回true。如果被檢測的對象是null或者基本類型,那麼返回值是false;一般形式如下:
//判斷這個對象能不能被轉化為這個類class.inInstance(obj)
最後這裡給出一個簡單實例,驗證isInstance方法與instanceof等價性:
執行結果:
Testing x of type class com.zejian.Ax instanceof A truex instanceof B false //父類不一定是子類的某個類型A.isInstance(x) trueB.isInstance(x) falsex.getClass() == A.class truex.getClass() == B.class falsex.getClass().equals(A.class)) truex.getClass().equals(B.class)) false---------------------------------------------Testing x of type class com.zejian.Bx instanceof A truex instanceof B trueA.isInstance(x) trueB.isInstance(x) truex.getClass() == A.class falsex.getClass() == B.class truex.getClass().equals(A.class)) falsex.getClass().equals(B.class)) true
到此關於Class對象相關的知識點都分析完了,下面將結合Class對象的知識點分析反射技術。
理解反射技術
反射機制是在運行狀態中,對於任意一個類,都能夠知道這個類的所有屬性和方法;對於任意一個對象,都能夠調用它的任意一個方法和屬性,這種動態獲取的信息以及動態調用對象的方法的功能稱為java語言的反射機制。一直以來反射技術都是Java中的閃亮點,這也是目前大部分框架(如Spring/Mybatis等)得以實現的支柱。在Java中,Class類與java.lang.reflect類庫一起對反射技術進行了全力的支持。在反射包中,我們常用的類主要有Constructor類表示的是Class 對象所表示的類的構造方法,利用它可以在運行時動態創建對象、Field表示Class對象所表示的類的成員變量,通過它可以在運行時動態修改成員變量的屬性值(包含private)、Method表示Class對象所表示的類的成員方法,通過它可以動態調用對象的方法(包含private),下面將對這幾個重要類進行分別說明。
Constructor類及其用法
Constructor類存在於反射包(java.lang.reflect)中,反映的是Class 對象所表示的類的構造方法。獲取Constructor對象是通過Class類中的方法獲取的,Class類與Constructor相關的主要方法如下:
下面看一個簡單例子來了解Constructor對象的使用:
運行結果:
User [age=20, name=Rollen]--------------------------------------------user1:User [age=22, name=xiaolong]--------------------------------------------user2:User [age=25, name=lidakang]--------------------------------------------構造函數[0]:private reflect.User(int,java.lang.String)參數類型[0]:(int,java.lang.String)構造函數[1]:public reflect.User(java.lang.String)參數類型[1]:(java.lang.String)構造函數[2]:public reflect.User()參數類型[2]:()
關於Constructor類本身一些常用方法如下(僅部分,其他可查API),
代碼演示如下:
其中關於Type類型這裡簡單說明一下,Type 是 Java 編程語言中所有類型的公共高級接口。它們包括原始類型、參數化類型、數組類型、類型變量和基本類型。getGenericParameterTypes 與 getParameterTypes 都是獲取構成函數的參數類型,前者返回的是Type類型,後者返回的是Class類型,由於Type頂級接口,Class也實現了該接口,因此Class類是Type的子類,Type 表示的全部類型而每個Class對象表示一個具體類型的實例,如String.class僅代表String類型。由此看來Type與 Class 表示類型幾乎是相同的,只不過 Type表示的範圍比Class要廣得多而已。當然Type還有其他子類,如:
- TypeVariable:表示類型參數,可以有上界,比如:T extends Number
- ParameterizedType:表示參數化的類型,有原始類型和具體的類型參數,比如:List
- WildcardType:表示通配符類型,比如:?, ? extends Number, ? super Integer
通過以上的分析,對於Constructor類已有比較清晰的理解,利用好Class類和Constructor類,我們可以在運行時動態創建任意對象,從而突破必須在編譯期知道確切類型的障礙。
Field類及其用法
Field 提供有關類或接口的單個字段的信息,以及對它的動態訪問權限。反射的字段可能是一個類(靜態)字段或實例字段。同樣的道理,我們可以通過Class類的提供的方法來獲取代表字段信息的Field對象,Class類與Field對象相關方法如下:
下面的代碼演示了上述方法的使用過程
上述方法需要注意的是,如果我們不期望獲取其父類的字段,則需使用Class類的getDeclaredField/getDeclaredFields方法來獲取字段即可,倘若需要連帶獲取到父類的字段,那麼請使用Class類的getField/getFields,但是也只能獲取到public修飾的的字段,無法獲取父類的私有字段。下面將通過Field類本身的方法對指定類屬性賦值,代碼演示如下:
其中的set(Object obj, Object value)方法是Field類本身的方法,用於設置字段的值,而get(Object obj)則是獲取字段的值,當然關於Field類還有其他常用的方法如下:
同樣通過案例演示上述方法:
在通過getMethods方法獲取Method對象時,會把父類的方法也獲取到,如上的輸出結果,把Object類的方法都打印出來了。而getDeclaredMethod/getDeclaredMethods方法都只能獲取當前類的方法。我們在使用時根據情況選擇即可。下面將演示通過Method對象調用指定類的方法:
在上述代碼中調用方法,使用了Method類的invoke(Object obj,Object... args)第一個參數代表調用的對象,第二個參數傳遞的調用方法的參數。這樣就完成了類方法的動態調用。
getReturnType方法/getGenericReturnType方法都是獲取Method對象表示的方法的返回類型,只不過前者返回的Class類型後者返回的Type(前面已分析過),Type就是一個接口而已,在Java8中新增一個默認的方法實現,返回的就參數類型信息
而getParameterTypes/getGenericParameterTypes也是同樣的道理,都是獲取Method對象所表示的方法的參數類型,其他方法與前面的Field和Constructor是類似的。
反射包中的Array類
在Java的java.lang.reflect包中存在著一個可以動態操作數組的類,Array,它提供了動態創建和訪問 Java 數組的方法。Array 允許在執行 get 或 set 操作進行取值和賦值。在Class類中與數組關聯的方法是:
java.lang.reflect.Array中的常用靜態方法如下:
下面通過一個簡單例子來演示這些方法
通過上述代碼演示,確實可以利用Array類和反射相結合動態創建數組,也可以在運行時動態獲取和設置數組中元素的值,其實除了上的set/get外Array還專門為8種基本數據類型提供特有的方法,如setInt/getInt、setBoolean/getBoolean,其他依次類推,需要使用是可以查看API文檔即可。除了上述動態修改數組長度或者動態創建數組或動態獲取值或設置值外,可以利用泛型動態創建泛型數組如下:
畢竟我們無法直接創建泛型數組,有了Array的動態創建數組的方式這個問題也就迎刃而解了。
//無效語句,編譯不通T[] a = new T[];
ok,到這反射中幾個重要並且常用的類我們都基本介紹完了,但更重要是,我們應該認識到反射機制並沒有什麼神奇之處。當通過反射與一個未知類型的對象打交道時,JVM只會簡單地檢查這個對象,判斷該對象屬於那種類型,同時也應該知道,在使用反射機制創建對象前,必須確保已加載了這個類的Class對象,當然這點完全不必由我們操作,畢竟只能JVM加載,但必須確保該類的”.class”文件已存在並且JVM能夠正確找到。關於Class類的方法在前面我們只是分析了主要的一些方法,其實Class類的API方法挺多的,建議查看一下API文檔,瀏覽一遍,有個印象也是不錯的選擇,這裡僅列出前面沒有介紹過又可能用到的API:
原文鏈接:http://blog.csdn.net/javazejian/article/details/70768369
閱讀更多 java全棧技術 的文章