深入理解Class對象
RRIT及Class對象的概念
RRIT(Run-Time Type Identification)運行時類型識別。在《Thinking in Java》一書第十四章中有提到,它的功能是在運行時識別對象的類型和類信息。有兩種主要方式:“傳統的”RTTI(它假定我們在編譯時已經知道所有類型)和“反射”機制(它允許我們在運行時發現和使用類信息)。
類是程序的一部分,每個類都有一個類對象。換句話說,無論何時編寫和編譯新類,都會生成一個Class對象(更恰當地說,保存在相同名稱的A.class文件中)。當第一次使用所有類時,它們都被動態地加載到JVM中。例如,我們編寫了一個Test類並編譯它來生成Test。班級。此時,Test類的Class對象保存在類文件中。當我們新建一個對象或引用一個靜態成員變量時,Java虛擬機(JVM)中的類加載器子系統將相應的類對象加載到JVM中,然后JVM從這個類型的信息中創建我們需要的類對象,或者提供靜態變量的參考值。應當注意,無論創建了多少實例對象,手動編寫的每個類類類在JVM中都只有一個Class對象,也就是說,每個類在內存中都具有並且只有一個對應的Class對象。
1 Test t1 = new Test(); 2 Test t2 = new Test(); 3 Test t3 = new Test();
如上所示,實際上JVM內存中只存有一個Test的Class對象。
Class類,類類也是Java中存在的一個真實類。JDK的Lang軟件包。類類的實例表示Java應用程序運行時的類枚舉或接口和注釋(每個Java類運行時被表示為JVM中的類對象),類對象可以通過類名來獲得。類,類型。getClass(),Class.forName(“類名”)。數組還映射到一個類對象,該類對象由具有相同元素類型和維度的所有數組共享。基本類型布爾、字節、char、.、int、long、float、double和關鍵詞void也表示為類對象。
1 ublic final class Class<T> implements java.io.Serializable, 2 GenericDeclaration, 3 Type, 4 AnnotatedElement { 5 private static final int ANNOTATION= 0x00002000; 6 private static final int ENUM = 0x00004000; 7 private static final int SYNTHETIC = 0x00001000; 8 9 private static native void registerNatives(); 10 static { 11 registerNatives(); 12 } 13 14 /* 15 * Private constructor. Only the Java Virtual Machine creates Class objects. //私有構造器,只有JVM才能調用創建Class對象 16 * This constructor is not used and prevents the default constructor being 17 * generated. 18 */ 19 private Class(ClassLoader loader) { 20 // Initialize final field for classLoader. The initialization value of non-null 21 // prevents future JIT optimizations from assuming this final field is null. 22 classLoader = loader; 23 }
到這我們也就可以得出以下幾點信息:
-
Class類也是類的一種,與class關鍵字是不一樣的。
-
手動編寫的類被編譯后會產生一個Class對象,其表示的是創建的類的類型信息,而且這個Class對象保存在同名.class的文件中(字節碼文件)
-
每個通過關鍵字class標識的類,在內存中有且只有一個與之對應的Class對象來描述其類型信息,無論創建多少個實例對象,其依據的都是用一個Class對象。
-
Class類只存私有構造函數,因此對應Class對象只能有JVM創建和加載
-
Class類的對象作用是運行時提供或獲得某個對象的類型信息,這點對於反射技術很重要(關於反射稍后分析)。
Class對象的加載及獲取
Class對象的加載
正如我們前面提到的,類對象是由JVM加載的,所以什么時候加載呢?實際上,所有類在第一次使用時都動態地加載到JVM中。當程序創建對該類的第一個靜態成員引用時,它加載使用的類(實際加載該類的字節碼文件)。注意,使用新操作符創建類的新實例對象也被視為對類的靜態成員(構造函數也是一個類)的引用。看來Java程序在開始運行之前沒有完全加載到內存中,而且它們的所有部分都按需加載。因此,當使用這個類時,類加載器首先檢查這個類的Class對象是否已經被加載(類的實例對象是根據Class對象中的類型信息創建的)。如果未加載,則默認的類加載Class對象將以相同的名稱保存。編譯后的類文件。當該類的字節碼文件被加載時,它們必須接受相關的驗證,以確保它們不被破壞,並且不包含壞的Java代碼(這是Java的安全機制檢測)。在沒有問題之后,它們將被動態地加載到內存中,這相當於Cl。ass對象被加載到內存中(畢竟,類字節碼文件保存Cl ass對象),並且還可以用於創建類的所有實例對象。

類加載的過程 :
1. 加載
在加載階段,虛擬機需要完成3件事:
(1)通過一個類的全限定名(org/fenixsoft/clazz/TestClass)獲取定義此類的二進制字節流(.class文件);
(2)將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構;
(3)在內存中生成一個代表這個類的 java.lang.Class 對象,作為方法區這個類的各種數據的訪問入口;
2. 驗證
驗證階段是非常重要的,這個階段是否嚴謹,直接決定了Java虛擬機是否能承受惡意代碼的攻擊,從執行性能的角度上講,驗證階段的工作量在虛擬機的類加載子系統中又占了相當大的一部分。驗證階段大致上完成下面4個階段的驗證動作:
(1)文件格式驗證
驗證字節流是否符合Class文件格式的規范,並且能被當前版本的虛擬機處理;
這階段的驗證是基於二進制字節流進行的,只有通過了這個階段的驗證,字節流才會進入內存的方法區進行儲存,所以后面的3個驗證階段全部是基於方法區的存儲結構進行的,不會再直接操作字節流。
(2)元數據驗證
對字節碼描述的信息進行語義分析,以保證其描述的信息符合Java語言規范的要求,保證不存在不符合Java語言規范的元數據信息;
(3)字節碼驗證
通過數據流和控制流分析,確定程序是語義是合法的、符合邏輯的,保證被校驗的方法在運行時不會做出危害虛擬機安全的事件;
(4)符號引用驗證
可以看作是對類自身以外(常量池中各種符號引用)的信息進行匹配性校驗,確保解析動作能正常執行;
3. 准備
准備階段是正式為類變量分配內存並設置類變量初始值階段,這些變量所使用的內存都將在方法區中進行分配。這里進行內存分配僅僅是類變量(被static修飾的變量),而不包括實例變量,實例變量將在對象實例化時隨着對象一起分配在Java堆中;
4. 解析
解析階段是虛擬機將常量池內的符號引用替換為直接引用的過程。解析動作主要針對類或接口、字段、類方法、方法類型、方法句柄和調用點限定符7類符號引用進行;
5. 初始化
初始化階段才真正開始執行類中定義的Java程序代碼(或者說是字節碼)。初始化是如何被觸發的:
(1)遇到new、getstatic、putstatic或involestatic這4條指令時;
(2)使用 java.lang.reflect 包的方法對類進行反射調用的時候;
(3)初始化一個類時,如果父類還沒被初始化,則先觸發父類的初始化;
(4)虛擬機啟動時,用戶需要指定一個要執行的主類 (包含main()方法的那個類),虛擬機會先初始化這個主類;
(5)如果一個 java.lang.invoke.MethodHandle 實例最后解析的結果是 REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,若句柄所對應的類沒有進行過初始化,則將它初始化;
上文源自《深入理解java虛擬機》一書,大家可以去讀一下,這本書基本上是java程序猿學習必讀之一了。在此就不深入展開了,因為這又是另一個JVM領域了。 以后如果寫了該方面的文章,會貼到這里。
Class對象的獲取
Class對象的獲取主要有3種:
- 通過實例getClass()方法獲取
- Class.forName方法獲取
- 類字面常量獲取
通過實例getClass()方法獲取
1 Test t1 = new Test(); 2 Class clazz=test.getClass(); 3 System.out.println("clazz:"+clazz.getName());
getClass()是從頂級類Object繼承而來的,它將返回表示該對象的實際類型的Class對象引用。
Class.forName方法獲取
forName方法是Class類的靜態成員方法,記住所有Class對象都源自這個Class類,因此Class類中定義的方法適用於所有Class對象。這里,通過forName方法,我們可以獲得Test類的相應Class對象引用。注意,當調用forName方法時,您需要捕獲一個名為ClassNotFoundException的異常,因為forName方法無法檢測編譯器中與其傳遞的字符串(是否有.class文件)對應的類的存在,並且只能在程序的運行時進行檢查。如果不存在,將引發ClassNotFoundException異常。
使用forName方式會觸發類的初始化,與之相比的是使用類字面常量獲取
類字面常量獲取
1 //字面常量的方式獲取Class對象 2 Class clazz = Test.class;
這不僅更簡單,而且更安全,因為它是在編譯時檢查的(因此不需要放在try語句塊中)。而且它消除了對forName()方法的調用,因此也更有效。注意,當您使用“.“類”創建對Class對象的引用,Class對象不會自動初始化。注意,當您使用“.“類”創建對Classs對象的引用,Class對象不會自動初始化。使用該類的准備實際上包括三個步驟:
- 加載,這是由類加載器執行的,該步驟將查找字節碼(通常在classpath所指定的路徑中查找,但這並非是必需的),並從這些字節碼中創建一個Class對象。
- 鏈接。在鏈接階段將驗證類中的字節碼,為靜態域分布存儲空間,並且如果必需的話,將解析這個類創建的對其他類的所有引用。
- 初始化。如果該類具有超類,則對其初始化,執行靜態初始化器和靜態初始化塊。
1 class Initable{ 2 static final int staticFinal = 47; 3 static final int staticFinal2 = ClassInitialization.rand.nextInt(1000); 4 static { 5 System.out.ptintln("Initializing Initable"); 6 } 7 } 8 9 class Initable2 { 10 static int staticNonFinal = 147; 11 static { 12 System.out.println("Initializing Initable2"); 13 } 14 } 15 16 class Initable3 { 17 static int staticNonFinal = 74; 18 static { 19 System.out.println("Initializing Initable3"); 20 } 21 } 22 23 public class ClassInitialization { 24 public static Random rand = new Random(47); 25 public static void main(String[] args) throws Exception { 26 Class initable = Initable.class; 27 System.out.println("After creating Initable ref"); 28 System.out.println(Initable.staticFinal); 29 System.out.println(Initable.staticFinal2); 30 System.out.println(Initable2.staticNonFinal); 31 Clas initable3 = Class.forName("Initable3"); 32 System.out.println("After creating Initable3 ref"); 33 System.out.println(Initable3.staticNonFinal); 34 } 35 } 36 37 /* output 38 After creating Initable ref 39 47 40 Initializing Initable 41 258 42 Initializing Initable2 43 147 44 Initializing Initable3 45 After creating Initable ref 46 74
如果一個static final值是編譯器常量
,就像Initable.staticFinal那樣,那么這個值不需要對Initable類進行初始化就可以被讀取。但是,如果只是將一個域設置為static和final的,還不足以確保這種行為,例如,對Initable.staticFinal2的訪問將強制進行類的初始化,因為它不是一個編譯期常量。
如果靜態域不是最終的,則總是需要在讀取之前進行鏈接(為域分配存儲空間)和初始化(初始化存儲空間),如訪問Initable2所示。靜態非決賽。從輸出結果可以看出,通過文字常數獲取獲得的Initable類的Class對象不觸發Initable類的初始化,這也驗證了前面的分析。同時,還發現不可逆的。staticFinal變量不觸發初始化,因為staticFinal在編譯時屬於靜態常數,並且在編譯階段通過常數傳播優化。公式將Initable類的常量staticFinal存儲在一個名為NotInitialization類的常量池中。將來,對常量staticFinal of Initable類的引用實際上被轉換成對其自己的常量NotInitialization類池的引用。因此,在編譯之后,對編譯時間常數的引用將在NotInitialization類的常量池中獲得,這也是引用編譯時間。靜態常數不觸發Initable類初始化的一個重要原因。然而,不適宜的。然后調用staticFinal2變量來觸發Initable類的初始化。注意,盡管staticFinal2由static和final修改,但它的值在編譯時無法確定。因此,staticFinal2不是編譯時間常數,在使用此變量之前,必須初始化Initable類。Initable2和Initable3是靜態成員變量,而不是編譯時常數,引用觸發初始化。至於forName方法獲取Class對象,初始化被綁定到.,前面已經對此進行了分析。
instanceof與Class的等價性
關於instanceof 關鍵字,它返回一個boolean類型的值,意在告訴我們對象是不是某個特定的類型實例。如下,在強制轉換前利用instanceof檢測obj是不是Animal類型的實例對象,如果返回true再進行類型轉換,這樣可以避免拋出類型轉換的異常(ClassCastException)

而isInstance方法則是Class類中的一個Native方法,也是用於判斷對象類型的,看個簡單例子:
事實上instanceOf 與isInstance方法產生的結果是相同的。
1 class A {} 2 3 class B extends A {} 4 5 public class C { 6 static void test(Object x) { 7 print("Testing x of type " + x.getClass()); 8 print("x instanceof A " + (x instanceof A)); 9 print("x instanceof B "+ (x instanceof B)); 10 print("A.isInstance(x) "+ A.class.isInstance(x)); 11 print("B.isInstance(x) " + 12 B.class.isInstance(x)); 13 print("x.getClass() == A.class " + 14 (x.getClass() == A.class)); 15 print("x.getClass() == B.class " + 16 (x.getClass() == B.class)); 17 print("x.getClass().equals(A.class)) "+ 18 (x.getClass().equals(A.class))); 19 print("x.getClass().equals(B.class)) " + 20 (x.getClass().equals(B.class))); 21 } 22 public static void main(String[] args) { 23 test(new A()); 24 test(new B()); 25 } 26 } 27 28 /* output 29 Testing x of type class com.zejian.A 30 x instanceof A true 31 x instanceof B false //父類不一定是子類的某個類型 32 A.isInstance(x) true 33 B.isInstance(x) false 34 x.getClass() == A.class true 35 x.getClass() == B.class false 36 x.getClass().equals(A.class)) true 37 x.getClass().equals(B.class)) false 38 --------------------------------------------- 39 Testing x of type class com.zejian.B 40 x instanceof A true 41 x instanceof B true 42 A.isInstance(x) true 43 B.isInstance(x) true 44 x.getClass() == A.class false 45 x.getClass() == B.class true 46 x.getClass().equals(A.class)) false 47 x.getClass().equals(B.class)) true
反射
反射機制是在運行狀態下可以知道任何類的所有屬性和方法,並且可以調用任何對象的任何方法和屬性。獲得的動態信息和對象的動態調用方法的功能被稱為Java語言的反射機制。反射技術一直是爪哇的一個亮點,它也是大多數框架(如Spring/MybATIS等)要實現的骨干。在Java中,類類和Java。反射類庫共同為反射技術提供了充分的支持。在反射包中,我們通常使用構造函數類來構造由類對象表示的類。通過使用Constructor類,我們可以在運行時動態創建對象以及由Class對象表示的Field類。通過使用Constructor類,我們可以在運行時動態修改成員變量(包括私有)和由Class對象表示的方法類的屬性值。方法,通過該方法可以動態調用對象的方法(包括私有類),下面將分別解釋這些重要的類。
Constructor類及其用法
Constructor類存在於反射包(java.lang.reflect)中,反映的是Class 對象所表示的類的構造方法。獲取Constructor對象是通過Class類中的方法獲取的,Class類與Constructor相關的主要方法如下:

下面看一個簡單例子來了解Constructor對象的使用:
1 public class ConstructionTest implements Serializable { 2 public static void main(String[] args) throws Exception { 3 4 Class<?> clazz = null; 5 6 //獲取Class對象的引用 7 clazz = Class.forName("com.example.javabase.User"); 8 9 //第一種方法,實例化默認構造方法,User必須無參構造函數,否則將拋異常 10 User user = (User) clazz.newInstance(); 11 user.setAge(20); 12 user.setName("Jack"); 13 System.out.println(user); 14 15 System.out.println("--------------------------------------------"); 16 17 //獲取帶String參數的public構造函數 18 Constructor cs1 =clazz.getConstructor(String.class); 19 //創建User 20 User user1= (User) cs1.newInstance("hiway"); 21 user1.setAge(22); 22 System.out.println("user1:"+user1.toString()); 23 24 System.out.println("--------------------------------------------"); 25 26 //取得指定帶int和String參數構造函數,該方法是私有構造private 27 Constructor cs2=clazz.getDeclaredConstructor(int.class,String.class); 28 //由於是private必須設置可訪問 29 cs2.setAccessible(true); 30 //創建user對象 31 User user2= (User) cs2.newInstance(25,"hiway2"); 32 System.out.println("user2:"+user2.toString()); 33 34 System.out.println("--------------------------------------------"); 35 36 //獲取所有構造包含private 37 Constructor<?> cons[] = clazz.getDeclaredConstructors(); 38 // 查看每個構造方法需要的參數 39 for (int i = 0; i < cons.length; i++) { 40 //獲取構造函數參數類型 41 Class<?> clazzs[] = cons[i].getParameterTypes(); 42 System.out.println("構造函數["+i+"]:"+cons[i].toString() ); 43 System.out.print("參數類型["+i+"]:("); 44 for (int j = 0; j < clazzs.length; j++) { 45 if (j == clazzs.length - 1) 46 System.out.print(clazzs[j].getName()); 47 else 48 System.out.print(clazzs[j].getName() + ","); 49 } 50 System.out.println(")"); 51 } 52 } 53 } 54 55 56 class User { 57 private int age; 58 private String name; 59 public User() { 60 super(); 61 } 62 public User(String name) { 63 super(); 64 this.name = name; 65 } 66 67 /** 68 * 私有構造 69 * @param age 70 * @param name 71 */ 72 private User(int age, String name) { 73 super(); 74 this.age = age; 75 this.name = name; 76 } 77 78 public int getAge() { 79 return age; 80 } 81 82 public void setAge(int age) { 83 this.age = age; 84 } 85 86 public String getName() { 87 return name; 88 } 89 90 public void setName(String name) { 91 this.name = name; 92 } 93 94 @Override 95 public String toString() { 96 return "User{" + 97 "age=" + age + 98 ", name='" + name + '\'' + 99 '}'; 100 } 101 } 102 103 /* output 104 User{age=20, name='Jack'} 105 -------------------------------------------- 106 user1:User{age=22, name='hiway'} 107 -------------------------------------------- 108 user2:User{age=25, name='hiway2'} 109 -------------------------------------------- 110 構造函數[0]:private com.example.javabase.User(int,java.lang.String) 111 參數類型[0]:(int,java.lang.String) 112 構造函數[1]:public com.example.javabase.User(java.lang.String) 113 參數類型[1]:(java.lang.String) 114 構造函數[2]:public com.example.javabase.User() 115 參數類型[2]:()
關於Constructor類本身一些常用方法如下(僅部分,其他可查API)
Field類及其用法
Field 提供有關類或接口的單個字段的信息,以及對它的動態訪問權限。反射的字段可能是一個類(靜態)字段或實例字段。同樣的道理,我們可以通過Class類的提供的方法來獲取代表字段信息的Field對象,Class類與Field對象相關方法如下:

下面的代碼演示了上述方法的使用過程
1 public class ReflectField { 2 3 public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException { 4 Class<?> clazz = Class.forName("reflect.Student"); 5 //獲取指定字段名稱的Field類,注意字段修飾符必須為public而且存在該字段, 6 // 否則拋NoSuchFieldException 7 Field field = clazz.getField("age"); 8 System.out.println("field:"+field); 9 10 //獲取所有修飾符為public的字段,包含父類字段,注意修飾符為public才會獲取 11 Field fields[] = clazz.getFields(); 12 for (Field f:fields) { 13 System.out.println("f:"+f.getDeclaringClass()); 14 } 15 16 System.out.println("================getDeclaredFields===================="); 17 //獲取當前類所字段(包含private字段),注意不包含父類的字段 18 Field fields2[] = clazz.getDeclaredFields(); 19 for (Field f:fields2) { 20 System.out.println("f2:"+f.getDeclaringClass()); 21 } 22 //獲取指定字段名稱的Field類,可以是任意修飾符的自動,注意不包含父類的字段 23 Field field2 = clazz.getDeclaredField("desc"); 24 System.out.println("field2:"+field2); 25 } 26 /** 27 輸出結果: 28 field:public int reflect.Person.age 29 f:public java.lang.String reflect.Student.desc 30 f:public int reflect.Person.age 31 f:public java.lang.String reflect.Person.name 32 33 ================getDeclaredFields==================== 34 f2:public java.lang.String reflect.Student.desc 35 f2:private int reflect.Student.score 36 field2:public java.lang.String reflect.Student.desc 37 */ 38 } 39 40 class Person{ 41 public int age; 42 public String name; 43 //省略set和get方法 44 } 45 46 class Student extends Person{ 47 public String desc; 48 private int score; 49 //省略set和get方法 50 }
這些方法可能更常用。實際上,Field類還提供了專門用於基本數據類型的方法,例如setInt()/getInt()、setBoolean()/getBoolean、setChar()/getChar()等等。這里沒有列出全部。您可以在需要時查看API文檔。應特別注意由final關鍵字修改的Field字段是安全的,並且可以在運行時接受任何修改,但其實際值最終不會改變。
Method類及其用法
Method 提供關於類或接口上單獨某個方法(以及如何訪問該方法)的信息,所反映的方法可能是類方法或實例方法(包括抽象方法)。下面是Class類獲取Method對象相關的方法:

同樣通過案例演示上述方法:
1 import java.lang.reflect.Method; 2 3 public class ReflectMethod { 4 5 6 public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException { 7 8 Class clazz = Class.forName("reflect.Circle"); 9 10 //根據參數獲取public的Method,包含繼承自父類的方法 11 Method method = clazz.getMethod("draw",int.class,String.class); 12 13 System.out.println("method:"+method); 14 15 //獲取所有public的方法: 16 Method[] methods =clazz.getMethods(); 17 for (Method m:methods){ 18 System.out.println("m::"+m); 19 } 20 21 System.out.println("========================================="); 22 23 //獲取當前類的方法包含private,該方法無法獲取繼承自父類的method 24 Method method1 = clazz.getDeclaredMethod("drawCircle"); 25 System.out.println("method1::"+method1); 26 //獲取當前類的所有方法包含private,該方法無法獲取繼承自父類的method 27 Method[] methods1=clazz.getDeclaredMethods(); 28 for (Method m:methods1){ 29 System.out.println("m1::"+m); 30 } 31 } 32 33 /** 34 輸出結果: 35 method:public void reflect.Shape.draw(int,java.lang.String) 36 37 m::public int reflect.Circle.getAllCount() 38 m::public void reflect.Shape.draw() 39 m::public void reflect.Shape.draw(int,java.lang.String) 40 m::public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException 41 m::public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException 42 m::public final void java.lang.Object.wait() throws java.lang.InterruptedException 43 m::public boolean java.lang.Object.equals(java.lang.Object) 44 m::public java.lang.String java.lang.Object.toString() 45 m::public native int java.lang.Object.hashCode() 46 m::public final native java.lang.Class java.lang.Object.getClass() 47 m::public final native void java.lang.Object.notify() 48 m::public final native void java.lang.Object.notifyAll() 49 50 ========================================= 51 method1::private void reflect.Circle.drawCircle() 52 53 m1::public int reflect.Circle.getAllCount() 54 m1::private void reflect.Circle.drawCircle() 55 */ 56 } 57 58 class Shape { 59 public void draw(){ 60 System.out.println("draw"); 61 } 62 63 public void draw(int count , String name){ 64 System.out.println("draw "+ name +",count="+count); 65 } 66 67 } 68 class Circle extends Shape{ 69 70 private void drawCircle(){ 71 System.out.println("drawCircle"); 72 } 73 public int getAllCount(){ 74 return 100; 75 } 76 }
