基本概念
在Object類中定義了以下的方法,此方法將被所有子類繼承
public final Class getClass()
這個方法的返回值類型是一個Class類,此類是Java反射的源頭,是實際上所謂反射從程序的運行結果來看也很好理解,即:可以通過對象反射求出類的名稱。
對象照鏡子后可以得到的信息:某個類的屬性、方法和構造器、某個類到底實現了哪些接口。對於每個類而言,JRE都為其保留一個不變的Class類型的對象。一個Class對象包含了特定某個結構(class、interface、enum、annotation、primitive type、void)的有關信息。
-
Class本身也是一個類
-
Class對象只能由系統建立對象
-
一個加載的類在JVM中只會有一個Class實例
-
一個Class對象對應的是一個加載到JVM中的一個.class文件
-
每個類的實例都會記得自己是由哪個Class實例所生成
-
通過Class可以完整地得到一個類中的所有被加載結構
-
Class類是Reflection的根源,針對任何你想動態加載、運行的類,唯有先獲得相應的Class對象。
Java中擁有Class對象的類型
在Java語言中,一切皆是對象。而對象主要分為兩種,一種是普通類創建的實例對象,一種是Class類對象。每個類運行時的類型信息就是通過Class對象表示的,這個對象包含了與類有關的信息。其實Java中的實例對象就是通過Class對象來創建的,Java使用Class對象執行其RTTI(運行時類型識別,Run-Time Type Identification),多態是基於RTTI實現的。
那么在Java中哪些類型可以有Class對象呢?
-
class:外部類、成員(成員內部類、靜態內部類)、局部內部類、匿名內部類
-
interface:接口
-
[]:數組
-
enum:枚舉
-
annotation:注解@interface
-
primitive type:基本數據類型
-
void
我們用代碼演示一下,這些類型的Class對象都是什么?
Class c1 = Object.class; // 類 Class c2 = Comparable.class; // 接口 Class c3 = String[].class; // 一維數組 Class c4 = int[][].class; // 二維數組 Class c5 = Override.class; // 注解 Class c6 = ElementType.class; // 枚舉 Class c7 = Integer.class; // 基本數據類型(包裝類) Class c10 = int.class; // 基本數據類型 Class c8 = void.class; // 空類型 Class c9 = Class.class; // Class System.out.println(c1); // class java.lang.Object System.out.println(c2); // interface java.lang.Comparable System.out.println(c3); // class [Ljava.lang.String; System.out.println(c4); // class [[I System.out.println(c5); // interface java.lang.Override System.out.println(c6); // class java.lang.annotation.ElementType System.out.println(c7); // class java.lang.Integer System.out.println(c10);// int System.out.println(c8); // void System.out.println(c9); // class java.lang.Class int[] a = new int[10]; int[] b = new int[100]; /* 對於數組,只要元素類型與維度一樣,就是同一個Class對象 */ System.out.println(a.getClass()); //class [I System.out.println(b.getClass()); //class [I System.out.println(b.getClass().hashCode()); //1735600054 System.out.println(a.getClass().hashCode()); //1735600054
每一個類都有一個Class對象,每當編譯一個新類就產生一個Class對象,更准確的來說,是被保存在一個同名的.class文件中。當程序中需要使用到這個類的時候(包括需要這個類的對象或引用這個類的靜態變量)就通過類加載器將類加到內存中來。
Class類沒有公共的構造方法,Class對象是在類加載的時候由Java虛擬機以及通過調用類加載器中的 defineClass 方法自動構造的,因此不能顯式地聲明一個Class對象。
獲取Class對象的方式有以下三種
-
Class.forName(“類的全限定名”)
-
實例對象.getClass()
-
類名.class (類字面常量)
Class.forName 和 實例對象.getClass()
Class.forName
方法是Class
類的一個靜態成員。forName
在執行的過程中發現如果傳入的參數類還沒有被加載,那么JVM就會調用類加載器去加載這個參數類,並返回加載后的Class對象。Class
對象和其他對象一樣,我們可以獲取並操作它的引用。在類加載的過程中,這個參數類的靜態語句塊會被執行。如果Class .forName
找不到你要加載的類,它會拋出ClassNotFoundException異常。
靜態語句塊(static)在類第一次被加載的時候被執行,Class對象僅在需要的時候才會被加載。
Class.forName
的好處就在於,不需要為了獲得Class
引用而持有該類型的對象,只要通過全限定名就可以返回該類型的一個Class
引用。如果你已經有了該類型的對象,那么我們就可以通過調用getClass()
方法來獲取Class
引用了,這個方法屬於根類Object
的一部分,它返回的是表示該對象的實際類型的Class
引用。
public class TestClass { public static void main(String[] args) throws ClassNotFoundException { Class.forName("反射.A"); B b = new B(); Class b2 = b.getClass(); } } class A{ static { System.out.println("My name is A"); } } class B{ static { System.out.println("My name is B"); } } /* 運行結果: My name is A My name is B */
可以看出,A和B這兩個類都有一個靜態語句塊,所以當使用Class.forName和new關鍵字在執行以后,A和B分別會加載到內存中,當這兩個類加載到內存以后,其內部的靜態語句塊便會被執行。與此同時,當用new關鍵字創建對象后,對應的實際創建這個對象的類已經裝載到內存中了,所以執行getClass()方法的時候,就不會再去執行類加載的操作了,而是直接從java堆中返回該類型的Class引用。
類字面常量
Java還提供了另一種方法來生成對Class對象的引用。即使用類字面常量,就像這樣:Cat.class,這樣做不僅更簡單,而且更安全,因為它在編譯時就會受到檢查(因此不需要置於try語句塊中)。並且根除了對forName()方法的調用,所有也更高效。類字面量不僅可以應用於普通的類,也可以應用於接口、數組及基本數據類型。
用.class來創建對Class對象的引用時,不會自動地初始化該Class對象(這點和Class.forName方法不同)。類對象的初始化階段被延遲到了對靜態方法或者非常數靜態域首次引用時才執行。
public class TestClass { public static void main(String[] args) throws ClassNotFoundException { /* 首先,調用class字面常量獲取Class對象 */ Class b = B.class; Class a = A.class; // 此時發現並沒有輸出A和B這兩個類的靜態語句塊的內容 // 說明此時並不會自動初始化該Class對象 System.out.println(A.a); System.out.println(B.s); /* My name is A 0 My name is B lalala */ } } class A{ static int a = 0; static { System.out.println("My name is A"); } } class B{ static String s = "lalala"; static { System.out.println("My name is B"); } }
發現當我們調用A和B的靜態成員的時候,這兩個類對象被初始化了,並且是先輸出了靜態語句塊的內容,然后才在主程序中輸出了兩個靜態成員變量的值。接下來我們將A類中的a變量改為常量static final int a = 0;
,在主程序再次調用時發現A中的靜態語句塊並沒被打印,說明引用常數靜態成員並不會使類對象初始化。
也可以看出,如果僅使用.class語法來獲得對類的Class引用是不會引發初始化的。但是如果使用Class.forName來產生引用,就會立即進行了初始化。
一旦類被加載了到了內存中,那么不論通過哪種方式獲得該類的Class對象,它們返回的都是指向同一個Java堆地址上的Class引用。JVM不會創建兩個相同類型的Class對象。其實對於任意一個Class對象,都需要由它的類加載器和這個類本身一同確定其在就Java虛擬機中的唯一性,也就是說,即使兩個Class對象來源於同一個Class文件,只要加載它們的類加載器不同,那這兩個Class對象就必定不相等。這里的“相等”包括了代表類的Class對象的equals()、isAssignableFrom()、isInstance()
等方法的返回結果,也包括了使用instanceof
關鍵字對對象所屬關系的判定結果。所以在Java虛擬機中使用雙親委派模型來組織類加載器之間的關系,來保證Class對象的唯一性。
參考博客地址: