一 RTTI概念
認識Class對象之前,先來了解一個概念,RTTI(Run-Time Type Identification)運行時類型識別,對於這個詞一直是 C++ 中的概念,至於Java中出現RTTI的說法則是源於《Thinking in Java》一書,其作用是在運行時識別一個對象的類型和類的信息,這里分兩種:
- 傳統的”RTTI”:它假定我們在編譯期已知道了所有類型(在沒有反射機制創建和使用類對象時,一般都是編譯期已確定其類型,如new對象時該類必須已定義好);
- 反射機制,它允許我們在運行時發現和使用類型的信息。
二 Class對象
在Java中用來表示運行時類型信息的對應類就是Class類,Class類也是一個實實在在的類,存在於JDK的java.lang包中,其部分源碼如下:
public final class Class<T> implements java.io.Serializable,GenericDeclaration,Type, AnnotatedElement { private static final int ANNOTATION= 0x00002000; private static final int ENUM = 0x00004000; private static final int SYNTHETIC = 0x00001000; private static native void registerNatives(); static { registerNatives(); } /* * Private constructor. Only the Java Virtual Machine creates Class objects.(私有構造,只能由JVM創建該類) * This constructor is not used and prevents the default constructor being * generated. */ private Class(ClassLoader loader) { // Initialize final field for classLoader. The initialization value of non-null // prevents future JIT optimizations from assuming this final field is null. classLoader = loader; }
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類的對象作用是運行時提供或獲得某個對象的類型信息,這點對於反射技術很重要(關於反射稍后分析)。
1、Class對象的加載
前面我們已提到過,Class對象是由JVM加載的,那么其加載時機是?
實際上所有的類都是在對其第一次使用時動態加載到JVM中的,當程序第一次調用類的靜態成員時,就會加載這個被使用的類(實際上加載的就是這個類的字節碼文件),注意,使用new操作符創建類的新實例對象也會被當作調用類的靜態成員(構造函數也是類的靜態方法),由此看來Java程序在它們開始運行之前並非被完全加載到內存的,其各個部分是按需加載,所以在使用該類時,類加載器首先會檢查這個類的Class對象是否已被加載(類的實例對象創建時依據Class對象中類型信息完成的),如果還沒有加載,默認的類加載器就會先根據類名查找.class文件(編譯后Class對象被保存在同名的.class文件中),在這個類的字節碼文件被加載時,它們必須接受相關驗證,以確保其沒有被破壞並且不包含不良Java代碼(這是java的安全機制檢測),完全沒有問題后就會被動態加載到內存中,此時相當於Class對象也就被載入內存了(畢竟.class字節碼文件保存的就是Class對象),同時Class對象也就可以被用來創建這個類的所有實例對象。
下面通過一個簡單例子來說明Class對象被加載的時機問題(例子引用自Thinking in Java):
class Candy { static { System.out.println("Loading Candy"); } } class Gum { static { System.out.println("Loading Gum"); } } class Cookie { static { System.out.println("Loading Cookie"); } } public class SweetShop { public static void print(Object obj) { System.out.println(obj); } public static void main(String[] args) { print("inside main"); new Candy(); print("After creating Candy"); try { Class.forName("Gum"); } catch(ClassNotFoundException e) { print("Couldn't find Gum"); } print("After Class.forName(\"Gum\")"); new Cookie(); print("After creating Cookie"); } }
在上述代碼中,每個類Candy、Gum、Cookie都存在一個static語句(static初始化是在類加載時進行的),這個語句會在類第一次被加載時執行,這個語句的作用就是告訴我們該類在什么時候被加載,執行結果:
inside main Loading Candy After creating Candy Loading Gum After Class.forName("Gum") Loading Cookie After creating Cookie
從結果來看,new一個Candy對象和Cookie對象,構造函數將被調用,屬於靜態方法的引用,Candy類的Class對象和Cookie的Class對象肯定會被加載,畢竟Candy實例對象的創建依據其Class對象。比較有意思的是:
Class.forName("Gum");
其中forName方法是Class類(所有Class對象都屬於這個類)的一個static成員方法。Class對象和其他對象一樣,我們可以獲取並操作它的引用(這就是類加載器的工作)。這里通過forName方法,我們可以獲取到Gum類對應的Class對象引用。從打印結果來看,調用forName方法將會導致Gum類被加載(前提是Gum類從來沒有被加載過)。
2、Class.forName()方法
通過上述的案例,我們知道Class.forName()方法的調用將會返回一個對應類的Class對象,因此如果我們想獲取一個類的運行時類型信息並加以使用時,可以調用Class.forName()方法獲取該類對應的Class對象的引用,這樣做的好處是無需通過持有該類的實例對象引用而去獲取Class對象,如下的第2種方式是通過一個實例對象獲取一個類的Class對象,其中的getClass()是從頂級類Object繼承而來的,它將返回表示該對象的實際類型的Class對象引用。
public static void main(String[] args) { try{ //通過Class.forName獲取Gum類的Class對象 Class clazz=Class.forName("Gum"); System.out.println("forName=clazz:"+clazz.getName()); }catch (ClassNotFoundException e){ e.printStackTrace(); } //通過實例對象獲取Gum的Class對象 Gum gum = new Gum(); Class clazz2 = gum.getClass(); System.out.println("new=clazz2:"+clazz2.getName());
}
注意調用forName方法時需要捕獲一個名稱為ClassNotFoundException的異常,因為forName方法在編譯器是無法檢測到其傳遞的字符串對應的類是否存在的,只能在程序運行時進行檢查,如果不存在就會拋出ClassNotFoundException異常。
3、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對象已創建完成,獲取其引用並不困難,而無需觸發類的最后階段初始化。下面通過小例子來驗證這個過程:
import java.util.*; class Initable { //編譯期靜態常量 static final int staticFinal = 47; //非編期靜態常量 static final int staticFinal2 = ClassInitialization.rand.nextInt(1000); static { System.out.println("Initializing Initable"); } } class Initable2 { //靜態成員變量 static int staticNonFinal = 147; static { System.out.println("Initializing Initable2"); } } class Initable3 { //靜態成員變量 static int staticNonFinal = 74; static { System.out.println("Initializing Initable3"); } } public class ClassInitialization { public static Random rand = new Random(47); public static void main(String[] args) throws Exception { //字面常量獲取方式獲取Class對象 Class initable = Initable.class; System.out.println("After creating Initable ref"); //不觸發類初始化 System.out.println(Initable.staticFinal); //會觸發類初始化 System.out.println(Initable.staticFinal2); //會觸發類初始化 System.out.println(Initable2.staticNonFinal); //forName方法獲取Class對象 Class initable3 = Class.forName("Initable3"); System.out.println("After creating Initable3 ref"); System.out.println(Initable3.staticNonFinal); } }
執行結果:
After creating Initable ref 47 Initializing Initable 258 Initializing Initable2 147 Initializing Initable3 After creating Initable3 ref 74
從輸出結果來看,可以發現,通過字面常量獲取方式獲取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對象的方式也都分析完,到此這里可以得出小結論:
- 獲取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的新增的動態語言支持,其關鍵特征是它的類型檢查的主體過程是在運行期而不是編譯期進行的,這是一個比較大點的話題,這里暫且打住)。
4、理解泛化的Class對象引用
由於Class的引用總是指向某個類的Class對象,利用Class對象可以創建實例類,這也就足以說明Class對象的引用指向的對象確切的類型。在Java SE5引入泛型后,我們可以利用泛型來表示Class對象更具體的類型,即使在運行期間會被擦除,但編譯期足以確保我們使用正確的對象類型。如下:
public class ClazzDemo { public static void main(String[] args){ //沒有泛型 Class intClass = int.class; //帶泛型的Class對象 Class<Integer> integerClass = int.class; integerClass = Integer.class; //沒有泛型的約束,可以隨意賦值 intClass= double.class; //編譯期錯誤,無法編譯通過 //integerClass = double.class } }
從代碼可以看出,聲明普通的Class對象,在編譯器並不會檢查Class對象的確切類型是否符合要求,如果存在錯誤只有在運行時才得以暴露出來。但是通過泛型聲明指明類型的Class對象,編譯器在編譯期將對帶泛型的類進行額外的類型檢查,確保在編譯期就能保證類型的正確性,實際上Integer.class就是一個Class<Integer>類的對象。面對下述語句,確實可能令人困惑,但該語句確實是無法編譯通過的。
//編譯無法通過 Class<Number> numberClass=Integer.class;
我們或許會想Integer不就是Number的子類嗎?然而事實並非這般簡單,畢竟Integer的Class對象並非Number的Class對象的子類,前面提到過,所有的Class對象都只來源於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<Number>是不一樣的。實際上,應該時刻記住向Class引用添加泛型約束僅僅是為了提供編譯期類型的檢查從而避免將錯誤延續到運行時期。
下面的示例使用了泛型類語法。它存儲了一個類引用,稍后又產生了一個List,填充這個List的對象是使用newInstance()方法,通過該引用生成的:
import java.util.*; class CountedInteger{ private static long counter; //每次創建一個實例,都會執行一次初始化id=counter++ private final long id = counter++; public String toString(){ return Long.toString(id); } } public class FilledList<T>{ private Class<T> type; public FilledList(Class<T> type){ this.type = type; } public List<T> create(int nElements){ List<T> result = new ArrayList<T>(); try{ for(int i=0;i<nElements;i++){ //CountedInteger必須提供默認的構造函數,通過CountedInteger類的Class對象創建一個實例對象 result.add(type.newInstance()); } }catch(Exception e){ throw new RuntimeException(e); } return result; } public static void main(String[] args){ FilledList<CountedInteger> f1 = new FilledList<CountedInteger>(CountedInteger.class); System.out.println(f1.create(15)); } }
輸出如下:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
5、關於類型轉換的問題
在Java SE5中新增一種使用Class對象進行類型轉換的方式,即cast()方式:
class Building{} class House extends Building{} public class ClassCasts { public static void main(String[] args) { Building b = new House(); Class<House> houseType = House.class; House h = houseType.cast(b); h = (House)b; } }
利用Class對象的cast()方法,其參數接收一個參數對象並將其轉換為Class引用的類型。當然,如果仔細觀察上面的代碼,則會發現,與實現了相同功能的main()中最后一行相比,這種轉型好像做了很多額外的工作。新的轉型語法對於無法使用普通類型的情況顯得非常有用,在編寫泛型代碼時,如果存儲了Class引用,並希望以后通過這個引用來執行轉型,這種情況就會時有發生。
6、instanceof 關鍵字與isInstance方法
關於instanceof 關鍵字,它返回一個boolean類型的值,意在告訴我們對象是不是某個特定的類型實例。如下,在強制轉換前利用instanceof檢測obj是不是Animal類型的實例對象,如果返回true再進行類型轉換,這樣可以避免拋出類型轉換的異常(ClassCastException)
public void cast(Object obj){ if(obj instanceof Animal){ Animal animal= (Animal) obj; } }
而isInstance方法則是Class類中的一個Native方法,也是用於判斷對象類型的,看個簡單例子:
public void cast2(Object obj){ //isInstance方法 if(Animal.class.isInstance(obj)){ Animal animal= (Animal) obj; } }
事實上instanceOf 與isInstance方法產生的結果是相同的。對於instanceof是關鍵字只被用於對象引用變量,檢查左邊對象是不是右邊類或接口的實例化。如果被測對象是null值,則測試結果總是false。一般形式:
//判斷這個對象是不是這種類型 obj instanceof className
而isInstance方法則是Class類的Native方法,其中obj是被測試的對象或者變量,如果obj是調用這個方法的class或接口的實例,則返回true。如果被檢測的對象是null或者基本類型,那么返回值是false;一般形式如下:
//判斷這個對象能不能被轉化為這個類 className.class.inInstance(obj)
最后這里給出一個簡單實例,驗證isInstance方法與instanceof等價性:
class A {} class B extends A {} public class Instance { public static void print(String msg) { System.out.println(msg); } public static void test(Object x) { print("Testing x of type: " + x.getClass()); print("x instanceof A: " + (x instanceof A)); print("x instanceof B: "+ (x instanceof B)); print("A.isInstance(x): "+ A.class.isInstance(x)); print("B.isInstance(x): " + B.class.isInstance(x)); print("x.getClass() == A.class " + (x.getClass() == A.class)); print("x.getClass() == B.class " + (x.getClass() == B.class)); print("x.getClass().equals(A.class)) "+ (x.getClass().equals(A.class))); print("x.getClass().equals(B.class)) " + (x.getClass().equals(B.class))); } public static void main(String[] args) { test(new A()); test(new B()); } }
輸出如下:
Testing x of type: class A x instanceof A: true x instanceof B: false A.isInstance(x): true B.isInstance(x): false x.getClass() == A.class true x.getClass() == B.class false x.getClass().equals(A.class)) true x.getClass().equals(B.class)) false Testing x of type: class B x instanceof A: true x instanceof B: true A.isInstance(x): true B.isInstance(x): true x.getClass() == A.class false x.getClass() == B.class true x.getClass().equals(A.class)) false x.getClass().equals(B.class)) true
到此關於Class對象相關的知識點都分析完了,下面將結合Class對象的知識點分析反射技術。
三 反射技術
反射機制是在運行狀態中,對於任意一個類,都能夠知道這個類的所有屬性和方法;對於任意一個對象,都能夠調用它的任意一個方法和屬性,這種動態獲取的信息以及動態調用對象的方法的功能稱為java語言的反射機制。一直以來反射技術都是Java中的閃亮點,這也是目前大部分框架(如Spring/Mybatis等)得以實現的支柱。在Java中,Class類與java.lang.reflect類庫一起對反射技術進行了全力的支持。在反射包中,我們常用的類主要有Constructor類表示的是Class 對象所表示的類的構造方法,利用它可以在運行時動態創建對象、Field表示Class對象所表示的類的成員變量,通過它可以在運行時動態修改成員變量的屬性值(包含private)、Method表示Class對象所表示的類的成員方法,通過它可以動態調用對象的方法(包含private),下面將對這幾個重要類進行分別說明。
1、Constructor類及其用法
Constructor類存在於反射包(java.lang.reflect)中,反映的是Class 對象所表示的類的構造方法。獲取Constructor對象是通過Class類中的方法獲取的,Class類與Constructor相關的主要方法如下:
| 方法返回值 | 方法名稱 | 方法說明 |
| static Class<?> | forName(String className) | 返回與帶有給定字符串名的類或接口相關聯的 Class 對象 |
| Constructor<T> | getConstructor(Class<?>... parameterTypes) | 返回指定參數類型、具有public訪問權限的構造函數對象 |
| Constructor<?>[] | getConstructors() | 返回所有具有public訪問權限的構造函數的Constructor對象數組 |
| Constructor<T> | getDeclaredConstructor(Class<?>... parameterTypes) | 返回指定參數類型、所有聲明的(包括private)構造函數對象 |
| Constructor<?>[] | getDeclaredConstructor() | 返回所有聲明的(包括private)構造函數對象 |
| T | newInstance() | 創建此 Class 對象所表示的類的一個新實例。 |
下面看一個簡單例子來了解Constructor對象的使用:
import java.io.Serializable; import java.lang.reflect.Constructor;
import java.lang.reflect.Type; class User { private int age; private String name; //無參數構造 public User() { super(); } public void setName(String name) { // TODO Auto-generated method stub this.name = name; } public void setAge(int i) { // TODO Auto-generated method stub this.age = i; } //一個參數構造 public User(String name) { super(); this.name = name; } /** * 私有構造 * @param age * @param name */ @SuppressWarnings("unused") private User(int age, String name) { super(); this.age = age; this.name = name; } @Override public String toString() { // TODO Auto-generated method stub return "My name is " + name + ", age is " + age; } } @SuppressWarnings("serial") public class ReflectDemo implements Serializable{ public static void main(String[] args) throws Exception { Class<?> clazz = null; //獲取User類對應的Class對象的引用 clazz = Class.forName("User"); //第一種方法,實例化默認構造方法,User必須有無參構造函數,否則將拋異常 User user = (User) clazz.newInstance(); user.setAge(20); user.setName("Rollen"); System.out.println("user:" + user); System.out.println("--------------------------------------------"); //獲取帶String參數的public構造函數 Constructor cs1 = clazz.getConstructor(String.class); //創建User User user1= (User) cs1.newInstance("Tom"); user1.setAge(22); System.out.println("user1:" + user1); System.out.println("--------------------------------------------"); //取得指定帶int和String參數構造函數,該方法是私有構造private Constructor cs2 = clazz.getDeclaredConstructor(int.class,String.class); //由於是private必須設置可訪問 cs2.setAccessible(true); //創建user對象 User user2= (User) cs2.newInstance(25,"Like"); System.out.println("user2:"+user2); System.out.println("--------------------------------------------"); //獲取所有構造包含private Constructor<?>[] cons = clazz.getDeclaredConstructors(); //查看每個構造方法需要的參數 for (int i = 0; i < cons.length; i++) { //獲取構造函數參數類型 Class<?> clazzs[] = cons[i].getParameterTypes(); System.out.println("構造函數["+i+"]:"+cons[i].toString() ); System.out.print("參數類型["+i+"]:("); for (int j = 0; j < clazzs.length; j++) { if (j == clazzs.length - 1) System.out.print(clazzs[j].getName()); else System.out.print(clazzs[j].getName() + ","); } System.out.println(")"); } } }
輸出如下:
user:My name is Rollen, age is 20 -------------------------------------------- user1:My name is Tom, age is 22 -------------------------------------------- user2:My name is Like, age is 25 -------------------------------------------- 構造函數[0]:private User(int,java.lang.String) 參數類型[0]:(int,java.lang.String) 構造函數[1]:public User(java.lang.String) 參數類型[1]:(java.lang.String) 構造函數[2]:public User() 參數類型[2]:()
關於Constructor類本身一些常用方法如下(僅部分,其他可查API),
| 方法返回值 | 方法名稱 | 方法說明 |
| Class<T> | getDeclaringClass() | 返回 Class 對象,該對象表示聲明由此 Constructor 對象表示的構造方法的類,其實就是返回真實類型(不包含參數)。 |
| Type[] | getGenericParameterTypes() | 按照聲明順序返回一組 Type 對象,返回的就是 Constructor對象構造函數的形參類型。 |
| String | getName() | 以字符串形式返回此構造方法的名稱。 |
| Class<?>[] | getParameterTypes() | 按照聲明順序返回一組 Class 對象,即返回Constructor 對象所表示構造方法的形參類型。 |
| T | newInstance(Object... initargs) | 使用此 Constructor對象表示的構造函數來創建新實例。 |
| String | toGenericString() | 返回描述此 Constructor 的字符串,其中包括類型參數。 |
在上面程序追加如下內容:
System.out.println("--------------------------------------------");
//Constructor類本身一些常用方法
System.out.println("------getDeclaredClass----------");
Class uclazz = cs2.getDeclaringClass();
//Constructor對象表示的構造方法的類
System.out.println("構造方法的類:" + uclazz.getName());
System.out.println("------getGenericParameterTypes--------");
//按照聲明順序返回一組 Type 對象,返回的就是 Constructor對象構造函數的形參類型
Type[] tps = cs2.getGenericParameterTypes();
for(Type tp:tps) {
System.out.println("參數名稱tp:" + tp);
}
System.out.println("------getParameterTypes--------");
//按照聲明順序返回一組 Class 對象,返回的就是 Constructor對象構造函數的形參類型
Class[] clazzs = cs2.getParameterTypes();
for(Class claz:clazzs) {
System.out.println("參數名稱tp:" + claz.getName());
}
System.out.println("------getName-------");
//以字符串形式返回此構造方法的名稱
System.out.println("getName:" + cs2.getName());
System.out.println("------toGenericString-------");
//返回描述此 Constructor 的字符串,其中包括類型參數。
System.out.println("toGenericString:" + cs2.toGenericString());
輸出如下:
-------------------------------------------- ------getDeclaredClass---------- 構造方法的類:User ------getGenericParameterTypes-------- 參數名稱tp:int 參數名稱tp:class java.lang.String ------getParameterTypes-------- 參數名稱tp:int 參數名稱tp:java.lang.String ------getName------- getName:User ------toGenericString------- toGenericString:private User(int,java.lang.String)
其中關於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<String>;
- WildcardType:表示通配符類型,比如:?, ? extends Number, ? super Integer;
通過以上的分析,對於Constructor類已有比較清晰的理解,利用好Class類和Constructor類,我們可以在運行時動態創建任意對象,從而突破必須在編譯期知道確切類型的障礙。
2、Field類及其用法
Field 提供有關類或接口的單個字段的信息,以及對它的動態訪問權限。反射的字段可能是一個類(靜態)字段或實例字段。同樣的道理,我們可以通過Class類的提供的方法來獲取代表字段信息的Field對象,Class類與Field對象相關方法如下:
| 方法返回值 | 方法名稱 | 方法說明 |
| Field | getDeclaredField(String name) | 獲取指定name名稱的(包含private修飾的)字段,不包括繼承的字段 |
| Field[] | getDeclaredFields() | 獲取Class對象所表示的類或接口的所有(包含private修飾的)字段,不包括繼承的字段 |
| Field | getField(String name) | 獲取指定name名稱、具有public修飾的字段,包含繼承字段 |
| Field[] | getFields() | 獲取修飾符為public的字段,包含繼承字段 |
下面的代碼演示了上述方法的使用過程:
import java.lang.reflect.*; class Person{ public int age; public String name; } class Student extends Person{ public String desc; private int score; } public class ReflectField { public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, SecurityException { Class<?> clazz = Class.forName("Student"); //獲取指定name名稱、具有public修飾的字段,包含繼承字段 Field field = clazz.getField("age"); System.out.println("field:" + field); System.out.println("-----------------------"); //獲取修飾符為public的字段,包含繼承字段 Field[] fields = clazz.getFields(); for(Field f:fields) { System.out.println("field:" + f); } System.out.println("-----------------------"); //獲取指定name名稱的(包含private修飾的)字段,不包括繼承的字段 Field field2 = clazz.getDeclaredField("desc"); System.out.println("field2:" + field2); System.out.println("-----------------------"); //獲取Class對象所表示的類或接口的所有(包含private修飾的)字段,不包括繼承的字段 Field[] fields2 = clazz.getDeclaredFields(); for(Field f:fields2) { System.out.println("field:" + f); } System.out.println("-----------------------"); } }
輸出結果如下:
field:public int Person.age ----------------------- field:public java.lang.String Student.desc field:public int Person.age field:public java.lang.String Person.name ----------------------- field2:public java.lang.String Student.desc ----------------------- field:public java.lang.String Student.desc field:private int Student.score -----------------------
上述方法需要注意的是,如果我們不期望獲取其父類的字段,則需使用Class類的getDeclaredField()/getDeclaredFields()方法來獲取字段即可,倘若需要連帶獲取到父類的字段,那么請使用Class類的getField()/getFields(),但是也只能獲取到public修飾的的字段,無法獲取父類的私有字段。下面將通過Field類本身的方法對指定類屬性賦值,代碼演示如下:
Student st = (Student)clazz.newInstance(); //獲取父類public字段並賦值 Field ageField = clazz.getField("age"); ageField.set(st, 18); Field nameField = clazz.getField("name"); nameField.set(st, "王傑文"); //只獲取當前類的字段,不獲取父類的字段 Field descField = clazz.getDeclaredField("desc"); descField.set(st, "I'm a Student!"); Field scoreField = clazz.getDeclaredField("score"); //設置為可訪問,score是private scoreField.setAccessible(true); scoreField.set(st, 88); System.out.println(st);
輸出如下:
name:王傑文 age:18 desc:I'm a Student! score:88
其中的set(Object obj, Object value)方法是Field類本身的方法,用於設置字段的值,相反get(Object obj)則是獲取字段的值,當然關於Field類還有其他常用的方法如下:
| 方法返回值 | 方法名稱 | 方法說明 |
| void | set(Object obj, Object value) | 將指定對象變量上此 Field 對象表示的字段設置為指定的新值。 |
| Object | get(Object obj) | 返回指定對象上此 Field 表示的字段的值 |
| Class<?> | getType() | 返回一個 Class 對象,它標識了此Field 對象所表示字段的聲明類型。 |
| boolean | isEnumConstant() | 如果此字段表示枚舉類型的元素則返回 true;否則返回 false |
| String | toGenericString() | 返回一個描述此 Field(包括其一般類型)的字符串 |
| String | getName() | 返回此 Field 對象表示的字段的名稱 |
| Class<?> | getDeclaringClass() | 返回表示類或接口的 Class 對象,該類或接口聲明由此 Field 對象表示的字段 |
| void | setAccessible(boolean flag) | 將此對象的 accessible 標志設置為指示的布爾值,即設置其可訪問性 |
上述方法可能是較為常用的,事實上在設置值的方法上,Field類還提供了專門針對基本數據類型的方法,如setInt()/getInt()、setBoolean()/getBoolean、setChar()/getChar()等等方法,這里就不全部列出了,需要時查API文檔即可。需要特別注意的是被final關鍵字修飾的Field字段是安全的,在運行時可以接收任何修改,但最終其實際值是不會發生改變的。
注意:使用反射在final字段上的修改是安全的,運行時系統會在不拋異常的情況下接受任何修改嘗試,但是實際上不會發生任何修改。
3、Method類及其用法
Method 提供關於類或接口上單獨某個方法(以及如何訪問該方法)的信息,所反映的方法可能是類方法或實例方法(包括抽象方法)。下面是Class類獲取Method對象相關的方法:
| 方法返回值 | 方法名稱 | 方法說明 |
| Method | getDeclaredMethod(String name, Class<?>... parameterTypes) | 返回一個指定參數的Method對象,該對象反映此 Class 對象所表示的類或接口的方法,該方法可以是公共、保護、默認(包)訪問或者私有方法,但不可以是繼承的方法 |
| Method[] | getDeclaredMethod() | 返回 Method 對象的一個數組,這些對象反映此 Class 對象表示的類或接口聲明的所有方法,包括公共、保護、默認(包)訪問和私有方法,但不包括繼承的方法 |
| Method | getMethod(String name, Class<?>... parameterTypes) | 返回一個 Method 對象,它反映此 Class 對象所表示的類或接口的public方法,包括繼承的方法 |
| Method[] | getMethods() | 返回一個包含某些 Method 對象的數組,這些對象反映此 Class 對象所表示的類或接口(包括那些由該類或接口聲明的以及從超類和超接口繼承的那些的類或接口)的所有public方法 |
同樣通過案例演示上述方法:
import java.lang.reflect.*; class Shape{ public void draw() { System.out.println("draw"); } public void draw(int count,String name) { System.out.println("draw" + name + ",count=" + count); } } class Circle extends Shape{ private void drawCircle() { System.out.println("drawCircle"); } public int getAllCount() { return 100; } } public class ReflectMethod { public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, SecurityException { Class clazz = Class.forName("Circle"); //返回一個 Method 對象,它反映此 Class 對象所表示的類或接口的public方法,包括繼承的方法 Method method = clazz.getMethod("draw", int.class,String.class); System.out.println("method:" + method); System.out.println("-----------------------------------------"); //返回一個包含某些 Method 對象的數組,這些對象反映此 Class 對象所表示的類或接口(包括那些由該類或接口聲明的以及從超類和超接口繼承的那些的類或接口)的所有public方法 Method[] methods = clazz.getMethods(); for(Method m:methods) { System.out.println("method:" + m); } System.out.println("-----------------------------------------"); //返回一個指定參數的Method對象,該對象反映此 Class 對象所表示的類或接口的方法,該方法可以是公共、保護、默認(包)訪問或者私有方法,但不可以是繼承的方法 Method method1 = clazz.getDeclaredMethod("drawCircle"); System.out.println("method:" + method1); System.out.println("-----------------------------------------"); //返回 Method 對象的一個數組,這些對象反映此 Class 對象表示的類或接口聲明的所有方法,包括公共、保護、默認(包)訪問和私有方法,但不包括繼承的方法 Method[] methods1 = clazz.getDeclaredMethods(); for(Method m:methods1) { System.out.println("method:" + m); } System.out.println("-----------------------------------------"); } }
輸出如下:
method:public void Shape.draw(int,java.lang.String) ----------------------------------------- method:public int Circle.getAllCount() method:public void Shape.draw() method:public void Shape.draw(int,java.lang.String) method:public final void java.lang.Object.wait() throws java.lang.InterruptedException method:public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException method:public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException method:public boolean java.lang.Object.equals(java.lang.Object) method:public java.lang.String java.lang.Object.toString() method:public native int java.lang.Object.hashCode() method:public final native java.lang.Class java.lang.Object.getClass() method:public final native void java.lang.Object.notify() method:public final native void java.lang.Object.notifyAll() ----------------------------------------- method:private void Circle.drawCircle() ----------------------------------------- method:private void Circle.drawCircle() method:public int Circle.getAllCount() -----------------------------------------
在通過getMethods()方法獲取Method對象時,會把父類的方法也獲取到,如上的輸出結果,把Object類的方法都打印出來了。而getDeclaredMethod()/getDeclaredMethods()方法都只能獲取當前類的方法。我們在使用時根據情況選擇即可。下面將演示通過Method對象調用指定類的方法:
//創建對象 Circle circle = (Circle) clazz.newInstance(); //通過Method對象的invoke(Object obj,Object... args)方法調用 method.invoke(circle,15,"圈圈"); //修改私有方法的訪問標識 method1.setAccessible(true); method1.invoke(circle); //對有返回值得方法操作 Method method2 = clazz.getDeclaredMethod("getAllCount"); Integer count = (Integer) method2.invoke(circle); System.out.println("count:" + count);
輸出如下:
draw:圈圈,count=15
drawCircle
count:100
在上述代碼中調用方法,使用了Method類的invoke(Object obj,Object... args)第一個參數代表調用的對象,第二個參數傳遞的調用方法的參數。這樣就完成了類方法的動態調用。
| 方法返回值 | 方法名稱 | 方法說明 |
| Object | invoke(Object obj, Object... args) | 對帶有指定參數的指定對象調用由此 Method 對象表示的底層方法 |
| Class<?> | getReturnType() | 返回一個 Class 對象,該對象描述了此 Method 對象所表示的方法的正式返回類型,即方法的返回類型 |
| Type | getGenericReturnType() | 返回表示由此 Method 對象所表示方法的正式返回類型的 Type 對象,也是方法的返回類型 |
| Class<?>[] | getParameterTypes() | 按照聲明順序返回 Class 對象的數組,這些對象描述了此 Method 對象所表示的方法的形參類型。即返回方法的參數類型組成的數組 |
| Type[] | getGenericParameterTypes() | 按照聲明順序返回 Type 對象的數組,這些對象描述了此 Method 對象所表示的方法的形參類型的,也是返回方法的參數類型 |
| String | getName() | 以 String 形式返回此 Method 對象表示的方法名稱,即返回方法的名稱 |
| boolean | isVarArgs() | 判斷方法是否帶可變參數,如果將此方法聲明為帶有可變數量的參數,則返回 true;否則,返回 false |
| String | toGenericString() | 返回描述此 Method 的字符串,包括類型參數 |
getReturnType()方法/getGenericReturnType()方法都是獲取Method對象表示的方法的返回類型,只不過前者返回的Class類型后者返回的Type(前面已分析過),Type就是一個接口而已,在Java8中新增一個默認的方法實現,返回的就是參數類型信息。
而getParameterTypes()/getGenericParameterTypes()也是同樣的道理,都是獲取Method對象所表示的方法的參數類型,其他方法與前面的Field和Constructor是類似的。
4、反射包中的Array類
在Java的java.lang.reflect包中存在着一個可以操作數組的類,Array,它提供了創建和訪問Java 數組的方法。Array允許在執行 get()或 set()操作進行取值和賦值。在Class類中與數組關聯的方法是:
| 方法返回值 | 方法名稱 | 方法說明 |
| Class<?> | getComponentType() | 返回表示數組元素類型的Class對象,即數組的類型 |
| boolean | isArray() | 判定此 Class 對象是否表示一個數組類 |
java.lang.reflect.Array中的常用靜態方法如下:
| 方法返回值 | 方法名稱 | 方法說明 |
| static Object | set(Object array, int index) | 返回指定數組對象中索引組件的值 |
| static int | getLength(Object array) | 以 int 形式返回指定數組對象的長度 |
| static object | newInstance(Class<?> componentType, int... dimensions) | 創建一個具有指定類型和維度的新數組 |
| static Object | newInstance(Class<?> componentType, int length) | 創建一個具有指定的組件類型和長度的新數組 |
| static void | set(Object array, int index, Object value) | 將指定數組對象中索引組件的值設置為指定的新值 |
下面通過一個簡單例子來演示這些方法:
import java.lang.reflect.*; public class ReflectArray { public static void main(String[] args) throws ClassNotFoundException { int[] array = { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; //獲取數組類型的Class對象 即int.class Class<?> clazz = array.getClass().getComponentType(); //創建一個具有指定的組件類型和長度的新數組。 //第一個參數:指定了數組中的每個元素應該是什么類型,第二個參數:數組的長度 Object newArr = Array.newInstance(clazz, 15); //獲取原數組的長度 int co = Array.getLength(array); //賦值原數組到新數組 System.arraycopy(array, 0, newArr, 0, co); for (int i:(int[]) newArr) { System.out.print(i+","); } //創建了一個長度為10 的字符串數組, //接着把索引位置為6 的元素設為"hello world!",然后再讀取索引位置為6 的元素的值 Class clazz2 = Class.forName("java.lang.String"); //創建一個長度為10的字符串數組,在Java中數組也可以作為Object對象 Object array2 = Array.newInstance(clazz2, 10); //把字符串數組對象的索引位置為6的元素設置為"hello" Array.set(array2, 6, "hello world!"); //獲得字符串數組對象的索引位置為5的元素的值 String str = (String)Array.get(array2, 6); System.out.println(); System.out.println(str);//hello } }
輸出結果:
1,2,3,4,5,6,7,8,9,0,0,0,0,0,0,
hello world!
通過上述代碼演示,確實可以利用Array類和反射相結合創建數組,也可以在運行時獲取和設置數組中元素的值,其實除了上的set()/get()外Array還專門為8種基本數據類型提供特有的方法,如setInt()/getInt()、setBoolean()/getBoolean(),其他依次類推,需要使用是可以查看API文檔即可。除了上述修改數組長度或者創建數組或獲取值或設置值外,可以利用泛型創建泛型數組如下:
import java.lang.reflect.*; public class ReflectArray { /** * 接收一個泛型數組,然后創建一個長度與接收的數組長度一樣的泛型數組, * 並把接收的數組的元素復制到新創建的數組中, * 最后找出新數組中的最小元素,並打印出來 * @param a * @param <T> */ public static <T extends Comparable<T>> void FindMinValue(T[] a) { //通過反射機制創建相同類型的數組 T[] b = (T[]) Array.newInstance(a.getClass().getComponentType(), a.length); for(int i=0;i<a.length;i++) { b[i] = a[i]; } T min = null; boolean flag = true; for(int i=0;i<b.length;i++) { if(flag) { min = b[i]; flag = false; } if(b[i].compareTo(min) < 0) { min = b[i]; } } System.out.println(min); } public static void main(String[] args) throws ClassNotFoundException { int[] array = { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; //獲取數組類型的Class對象 即int.class Class<?> clazz = array.getClass().getComponentType(); //創建一個具有指定的組件類型和長度的新數組。 //第一個參數:指定了數組中的每個元素應該是什么類型,第二個參數:數組的長度 Object newArr = Array.newInstance(clazz, 15); //獲取原數組的長度 int co = Array.getLength(array); //賦值原數組到新數組 System.arraycopy(array, 0, newArr, 0, co); for (int i:(int[]) newArr) { System.out.print(i+","); } //創建了一個長度為10 的字符串數組, //接着把索引位置為6 的元素設為"hello world!",然后再讀取索引位置為6 的元素的值 Class clazz2 = Class.forName("java.lang.String"); //創建一個長度為10的字符串數組,在Java中數組也可以作為Object對象 Object array2 = Array.newInstance(clazz2, 10); //把字符串數組對象的索引位置為6的元素設置為"hello" Array.set(array2, 6, "hello world!"); //獲得字符串數組對象的索引位置為5的元素的值 String str = (String)Array.get(array2, 6); System.out.println(); System.out.println(str);//hello String[] strs = {"za","cb","ca","d","e","f"}; FindMinValue(strs); } }
輸出如下:
1,2,3,4,5,6,7,8,9,0,0,0,0,0,0, hello world! ca
畢竟我們無法直接創建泛型數組,有了Array創建泛型數組的問題也就迎刃而解了。
//無效語句,編譯不通 T[] b = new T[a.length];
到這反射中幾個重要並且常用的類我們都基本介紹完了,但更重要是,我們應該認識到反射機制並沒有什么神奇之處。當通過反射與一個未知類型的對象打交道時,JVM只會簡單地檢查這個對象,判斷該對象屬於哪種類型,同時也應該知道,在使用反射機制創建對象前,必須確保已加載了這個類的Class對象,當然這點完全不必由我們操作,畢竟只能JVM加載,但必須確保該類的”.class”文件已存在並且JVM能夠正確找到。關於Class類的方法在前面我們只是分析了主要的一些方法,其實Class類的API方法挺多的,建議查看一下API文檔,瀏覽一遍,有個印象也是不錯的選擇,這里僅列出前面沒有介紹過又可能用到的API:
/** * 修飾符、父類、實現的接口、注解相關 */ //獲取修飾符,返回值可通過Modifier類進行解讀 public native int getModifiers(); //獲取父類,如果為Object,父類為null public native Class<? super T> getSuperclass(); //對於類,為自己聲明實現的所有接口,對於接口,為直接擴展的接口,不包括通過父類間接繼承來的 public native Class<?>[] getInterfaces(); //自己聲明的注解 public Annotation[] getDeclaredAnnotations(); //所有的注解,包括繼承得到的 public Annotation[] getAnnotations(); //獲取或檢查指定類型的注解,包括繼承得到的 public <A extends Annotation> A getAnnotation(Class<A> annotationClass); public boolean isAnnotationPresent(Class<? extends Annotation> annotationClass); /** * 內部類相關 */ //獲取所有的public的內部類和接口,包括從父類繼承得到的 public Class<?>[] getClasses(); //獲取自己聲明的所有的內部類和接口 public Class<?>[] getDeclaredClasses(); //如果當前Class為內部類,獲取聲明該類的最外部的Class對象 public Class<?> getDeclaringClass(); //如果當前Class為內部類,獲取直接包含該類的類 public Class<?> getEnclosingClass(); //如果當前Class為本地類或匿名內部類,返回包含它的方法 public Method getEnclosingMethod(); /** * Class對象類型判斷相關 */ //是否是數組 public native boolean isArray(); //是否是基本類型 public native boolean isPrimitive(); //是否是接口 public native boolean isInterface(); //是否是枚舉 public boolean isEnum(); //是否是注解 public boolean isAnnotation(); //是否是匿名內部類 public boolean isAnonymousClass(); //是否是成員類 public boolean isMemberClass(); //是否是本地類 public boolean isLocalClass();
四 動態代理
動態代理看起來好像是個什么高大上的名詞,但其實並沒有那么復雜,直接從字面就很容易理解。動態地代理,可以猜測一下它的含義,在運行時動態地對某些東西代理,代理它做了其他事情。先不去搞清楚這個動態代理真正的含義,我們來舉個生動的例子來理解下它到底做了什么。
1、簡單案例
一個程序員Developer,他會開發code,他調試debug:
interface Developer{ void code(); void debug(); }
程序員有很多分類,其中有Java程序員JavaDeveloper,他會開發Java代碼,會調試Java代碼。
class JavaDeveloper implements Developer{ private String name; public JavaDeveloper(String name) { this.name = name; } @Override public void code() { // TODO Auto-generated method stub System.out.println(this.name + " is coding java"); } @Override public void debug() { // TODO Auto-generated method stub System.out.println(this.name + " is debugging java"); } }
其實我們沒有必要去定義他,因為他是后天養成的,我們應該在這個程序員的成長期去實現這個特性,而不是在他出生之前定義。我們來看下代碼是怎么實現的:
public class JavaDynamicProxy { public static void main(String[] args) { JavaDeveloper zack = new JavaDeveloper("Zack"); zack.code(); zack.debug(); //創建動態代理 Developer zackProxy = (Developer)Proxy.newProxyInstance( zack.getClass().getClassLoader(), //類加載器 zack.getClass().getInterfaces(), //希望被代理的接口列表 (proxy,method,agrs) -> { if(method.getName().equals("code")) { System.out.println("Zack is praying for the code"); } if(method.getName().equals("debug")) { System.out.println("Zack's have no bug! No need to debug!"); } //return method.invoke(proxy, agrs); return null; } ); zackProxy.code(); zackProxy.debug(); } }
如果Zack只是一個普通的Java程序員,那么他的開發結果是:
Zack is coding java
Zack is debugging java
但是真正的Zack(代理后):
Zack is praying for the code Zack's have no bug! No need to debug!
2、Proxy.newProxyInstance()
回看下上面是如何使用動態代理的使用。生成一個實例對象zack,然后用Proxy的newInstance方法對這個實例對象代理生成一個動態代理對象zackProxy。
//創建動態代理 Developer zackProxy = (Developer)Proxy.newProxyInstance( zack.getClass().getClassLoader(), //類加載器 zack.getClass().getInterfaces(), //希望被代理的接口列表 (proxy,method,agrs) -> { if(method.getName().equals("code")) { System.out.println("Zack is praying for the code"); } if(method.getName().equals("debug")) { System.out.println("Zack's have no bug! No need to debug!"); } //return method.invoke(proxy, agrs); return null; } );
看下newProxyInstance()的接口定義:
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
這三個參數具體的含義來看看注解是怎么描述的:
- loader:選用的類加載器。因為代理的是zack,所以一般都會用加載zack的類加載器;
- interfaces:被代理的類所實現的接口,這個接口可以是多個;
- h:綁定代理類的一個方法,或者是InvacationHandler接口的一個實現;
因此可以修改第三個參數為InvacationHandler接口的一個實現,代碼如下:
import java.lang.reflect.*; interface Developer{ void code(); void debug(); } class JavaDeveloper implements Developer{ private String name; public JavaDeveloper(String name) { this.name = name; } @Override public void code() { // TODO Auto-generated method stub System.out.println(this.name + " is coding java"); } @Override public void debug() { // TODO Auto-generated method stub System.out.println(this.name + " is debugging java"); } } //調用處理器 class DynamicProxyHandlerDeveloper implements InvocationHandler{ private Object proxied; //傳入被代理對象 public DynamicProxyHandlerDeveloper(Object proxied) { this.proxied = proxied; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // TODO Auto-generated method stub if(method.getName().equals("code")) { System.out.println("Zack is praying for the code"); } if(method.getName().equals("debug")) { System.out.println("Zack's have no bug! No need to debug!"); } //return method.invoke(proxied, agrs); //被代理對象zack,這里可以將請求轉發給原本方法; //return method.invoke(proxy, agrs); //代理對象zackProxy,會陷入死循環 return null; } } public class JavaDynamicProxy { public static void main(String[] args) { JavaDeveloper zack = new JavaDeveloper("Zack"); zack.code(); zack.debug(); //創建動態代理 Developer zackProxy = (Developer)Proxy.newProxyInstance( zack.getClass().getClassLoader(), //類加載器 zack.getClass().getInterfaces(), //希望被代理的接口列表 new DynamicProxyHandlerDeveloper(zack) ); zackProxy.code(); zackProxy.debug(); } }
loader和interfaces基本就是決定了這個類到底是個怎么樣的類。而h是InvocationHandler,決定了這個代理類到底是多了什么功能。所以動態代理的內容重點就是這個InvocationHandler。
3、InvocationHandler
InvocationHandler作用就是,當被代理對象zack的原本方法被調用的時候,會綁定執行一個方法,這個方法就是InvocationHandler里面定義的內容,同時會替代原本方法的結果返回:
InvocationHandler接收三個參數
- proxy:動態代理類對象(zackProxy);
- method:被代理對象(zack)被調用方法;
- args:調用時的參數;
在上面的例子里:
(proxy,method,agrs) -> { if(method.getName().equals("code")) { System.out.println("Zack is praying for the code"); } if(method.getName().equals("debug")) { System.out.println("Zack's have no bug! No need to debug!"); } //return method.invoke(proxy, agrs); return null; }
如果最后的return語句改成:
return method.invoke(proxy, agrs);
是的,會不停地循環調用。因為proxy是代理類的對象,當該對象方法被調用的時候,會觸發InvocationHandler,而InvocationHandler里面又調用一次proxy里面的對象,所以會不停地循環調用。並且,proxy對應的方法是沒有實現的。所以是會循環的不停報錯。
4、動態代理的使用場景
動態代理的好處我們從例子就能看出來,它比較靈活,可以在運行的時候才切入改變類的方法,而不需要預先定義它。
動態代理一般我們比較少去手寫,但我們用得其實非常多。在Spring項目中用的注解,例如依賴注入的@Bean、@Autowired,事務注解@Transactional等都有用到,換言之就是Srping的AOP(切面編程)。
這種場景的使用是動態代理最佳的落地點,可以非常靈活地在某個類,某個方法,某個代碼點上切入我們想要的內容,就是動態代理其中的內容。
參考文章
[1] 深入理解Java類型信息(Class對象)與反射機制(轉載)
[2]Java編程思想
[3] 【譯】1. Java反射——引言
[4] 你真的完全了解Java動態代理嗎?看這篇就夠了(轉載)
