Java的Class類提供了很多的getDeclaredXXX方法和getXXX方法,例如getDeclaredMethod和getMethod成對出現,getDeclaredConstructors和getConstructors也是成對出現,那這兩者之間有什麼差別呢?看如下代碼:
public class Client102 { public static void main(String[] args) throws NoSuchMethodException, SecurityException { // 方法名稱 String methodName = "doStuff"; Method m1 = Foo.class.getDeclaredMethod(methodName); Method m2 = Foo.class.getMethod(methodName); } //靜態內部類 static class Foo { void doStuff() { } } }
此段代碼運行後輸出如下:
Exception in thread "main" java.lang.NoSuchMethodException: com.study.advice102.Client102$Foo.doStuff() at java.lang.Class.getMethod(Class.java:1622) at com.study.advice102.Client102.main(Client102.java:10)
該異常是說m2變量的getMethod方法沒有找到doStuff方法,明明有這個方法呀,為什麼沒有找到呢?這是因為getMethod方法獲得的是所有public訪問級別的方法,包括從父類繼承的方法,而getDeclaredMethod獲得的是自身類的方法,包括公用的(public)方法、私有(private)方法,而且不受限於訪問權限。
其它的getDeclaredConstructors和getConstructors、getDeclaredFileds和getFields等於此相似。Java之所以如此處理,是因為反射本意只是正常代碼邏輯的一種補充,而不是讓正常代碼邏輯發生翻天覆地的變化,所以public的屬性和方法最容易獲取,私有屬性和方法也可以獲取,但要限定本類。
那麼問題來了:如果需要列出所有繼承自父類的方法,該如何實現呢?簡單,先獲得父類,然後使用getDeclaredMethods,之後持續遞歸即可。
Java中通過反射執行一個方法的過程如下:獲取一個方法對象,然後根據isAccessible返回值確定是否能夠執行,如果返回值為false則需要調用setAccessible(true),最後再調用invoke執行方法,具體如下:
Method method= ...; //檢查是否可以訪問 if(!method.isAccessible()){ method.setAccessible(true); } //執行方法 method.invoke(obj, args);
此段代碼已經成了習慣用法:通過反射方法執行方法時,必須在invoke之前檢查Accessible屬性。這是一個好習慣,也確實該如此,但方法對象的Accessible屬性並不是用來決定是否可以訪問的,看如下代碼:
public class Foo { public final void doStuff(){ System.out.println("Do Stuff..."); } }
定義一個public類的public方法,這是一個沒有任何限制的方法,按照我們對Java語言的理解,此時doStuff方法可以被任何一個類訪問。我們編寫一個客戶端類來檢查該方法是否可以反射執行:
public static void main(String[] args) throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { // 反射獲取方法 Method m = Foo.class.getMethod("doStuff"); // 打印是否可以訪問 System.out.println("Accessible:" + m.isAccessible()); // 執行方法 m.invoke(new Foo()); }
很簡單的反射操作,獲得一個方法,然後檢查是否可以訪問,最後執行方法輸出。讓我們來猜想一下結果:因為Foo類是public的,方法也是public的,全部都是最開放的訪問權限Accessible也應該等於true。但是運行結果卻是:
Accessible:false
Do Stuff...
為什麼Accessible屬性會等於false?而且等於false還能執行?這是因為Accessible的屬性並不是我們語法層級理解的訪問權限,而是指是否更容易獲得,是否進行安全檢查。
我們知道,動態修改一個類或執行方法時都會受到Java安全體制的制約,而安全的處理是非常耗資源的(性能非常低),因此對於運行期要執行的方法或要修改的屬性就提供了Accessible可選項:由開發者決定是否要逃避安全體系的檢查。
閱讀源代碼是最好的理解方式,我們來看AccessibleObject類的源代碼,它提供了取消默認訪問控制檢查的功能。首先查看isAccessible方法,代碼如下:
public class AccessibleObject implements AnnotatedElement { //定義反射的默認操作權限suppressAccessChecks static final private java.security.Permission ACCESS_PERMISSION = new ReflectPermission("suppressAccessChecks"); //是否重置了安全檢查,默認為false boolean override; //構造函數 protected AccessibleObject() {} //是否可以快速獲取,默認是不能 public boolean isAccessible() { return override; } }
AccessibleObject是Filed、Method、Constructor的父類,決定其是否可以快速訪問而不進行訪問控制檢查,在AccessibleObject類中是以override變量保存該值的,但是具體是否快速執行時在Method的invoke方法中決定的,源碼如下:
public Object invoke(Object obj, Object... args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { //見擦汗是否可以快速獲取,其值是父類AccessibleObject的override變量 if (!override) { //不能快速獲取,執行安全檢查 if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) { Class<?> caller = Reflection.getCallerClass(1); checkAccess(caller, clazz, obj, modifiers); } } MethodAccessor ma = methodAccessor; // read volatile if (ma == null) { ma = acquireMethodAccessor(); } //直接執行方法 return ma.invoke(obj, args); }
看了這段代碼,大家就清楚了:Accessible屬性只是用來判斷是否需要進行安全檢查的,如果不需要則直接執行,這就可以大幅度的提升系統性能了(當然了,取消了安全檢查,也可以運行private方法、訪問private屬性的)。經過測試,在大量的反射情況下,設置Accessible為true可以提高性能20倍左右。
AccessibleObject的其它兩個子類Field和Constructor與Method的情形類似:Accessible屬性決定Field和Constructor是否受訪問控制檢查。我們在設置Field或執行Constructor時,務必要設置Accessible為true,這並不僅僅是因為操作習慣的問題,還是為我們的系統性能考慮。
動態加載(Dynamic Loading)是指在程序運行時加載需要的類庫文件,對Java程序來說,一般情況下,一個類文件在啟動時或首次初始化時會被加載到內存中,而反射則可以在運行時再決定是否需要加載一個類,比如從Web上接收一個String參數作為類名,然後在JVM中加載並初始化,這就是動態加載,此動態加載通常是通過Class.forName(String)實現的,只是這個forName方法到底是什麼意思呢?
我們知道一個類文件只有在被加載到內存中才可能生成實例對象,也就是說一個對象的生成必然會經過兩個步驟:
如果我們使用的是import關鍵字產生的依賴包,JVM在啟動時會自動加載所有的依賴包的類文件,這沒有什麼問題,如果好動態加載類文件,就要使用forName的方法了,但問題是我們為什麼要使用forName方法動態加載一個類文件呢?那是因為我們不知道生成的實例對象是什麼類型(如果知道就不用動態加載),而且方法和屬性都不可訪問呀。問題又來了:動態加載的意義在什麼地方呢?
意義在於:加載一個類即表示要初始化該類的static變量,特別是static代碼塊,在這裡我們可以做大量的工作,比如注冊自己,初始化環境等,這才是我們要重點關注的邏輯,例如如下代碼:
package com.study.advice103; public class Client103 { public static void main(String[] args) throws ClassNotFoundException { //動態加載 Class.forName("com.study.advice103.Utils"); } } class Utils{ //靜態代碼塊 static{ System.out.println("Do Something....."); } }
注意看Client103類,我們並沒有對Utils做任何初始化,只是通過forName方法加載了Utils類,但是卻產生了一個“Do Something.....”的輸出,這就是因為Utils類加載後,JVM會自動初始化其static變量和static靜態代碼塊,這是類加載機制所決定的。
對於動態加載,最經典的應用是數據庫驅動程序的加載片段,代碼如下:
//加載驅動 Class.forName("com.mysql..jdbc.Driver"); String url="jdbc:mysql://localhost:3306/db?user=&password="; Connection conn =DriverManager.getConnection(url); Statement stmt =conn.createStatement();
在沒有Hibernate和Ibatis等ORM框架的情況下,基本上每個系統都會有這麼一個JDBC鏈接類,然後提供諸如Query、Delete等的方法,大家有沒有想過為什麼要加上forName這句話呢?沒有任何的輸出呀,要它干什麼用呢?事實上非常有用,我們看一下Driver的源碼:
public class Driver extends NonRegisteringDriver implements java.sql.Driver { //構造函數 public Driver() throws SQLException { } //靜態代碼塊 static { try { //把自己注冊到DriverManager中 DriverManager.registerDriver(new Driver()); } catch(SQLException E) { //異常處理 throw new RuntimeException("Can't register driver!"); } } }
該程序的邏輯是這樣的:數據庫驅動程序已經由NonRegisteringDriver實現了,Driver類只是負責把自己注冊到DriverManager中。當程序動態加載該驅動時,也就是執行到Class.forName("com.mysql..jdbc.Driver")時,Driver類會被加載到內存中,於是static代碼塊開始執行,也就是把自己注冊到DriverManager中。
需要說明的是,forName只是把一個類加載到內存中,並不保證由此產生一個實例對象,也不會執行任何方法,之所以會初始化static代碼,那是由類加載機制所決定的,而不是forName方法決定的。也就是說,如果沒有static屬性或static代碼塊,forName就是加載類,沒有任何的執行行為。
注意:forName只是加載類,並不執行任何代碼。
上一個建議解釋了為什麼要用forName,本建議就來說說那些地方不適合動態加載。如果forName要加載一個類,那它首先必須是一個類___8個基本類型排除在外,它們不是一個具體的類;其次,它必須具有可追溯的類路徑,否則就會報ClassNotFoundException。
在Java中,數組是一個非常特殊的類,雖然它是一個類,但沒有定義類類路徑,例如這樣的代碼:
public static void main(String[] args) throws ClassNotFoundException { String [] strs = new String[10]; Class.forName("java.lang.String[]"); }
String []是一個類型聲明,它作為forName的參數應該也是可行的吧!但是非常遺憾,其運行結果如下:
Exception in thread "main" java.lang.ClassNotFoundException: java/lang/String[] at java.lang.Class.forName0(Native Method) at java.lang.Class.forName(Class.java:186)
產生ClassNotFoundException異常的原因是數組算是一個類,在聲明時可以定義為String[],但編譯器編譯後為不同的數組類型生成不同的類,具體如下表所示:
數組編譯對應關系表 元素類型 編譯後的類型 byte[] [B char[] [C Double[] [D Float[] [F Int[] [I Long[] [J Short[] [S Boolean[] [Z 引用類型(如String[]) [L引用類型(如:[Ljava.lang.String;)在編碼期,我們可以聲明一個變量為String[],但是經過編譯後就成為了[Ljava.lang.String。明白了這一點,再根據以上的表格可知,動態加載一個對象數組只要加載編譯後的數組對象就可以了,代碼如下:
//加載一個數組 Class.forName("[Ljava.lang.String;"); //加載一個Long數組 Class.forName("[J");
雖然以上代碼可以加載一個數組類,但這是沒有任何意義的,因為它不能產生一個數組對象,也就是說以上代碼只是把一個String類型的數組類和Long類型的數組類加載到了內存中(如果內存中沒有改類的話),並不能通過newInstance方法生成一個實例對象,因為它沒有定義數組的長度,在Java中數組是定長的,沒有長度的數組是不允許存在的。
既然反射不能定義一個數組,那問題就來了:如何動態加載一個數組呢?比如依據輸入動態生成一個數組。其實可以使用Array數組反射類動態加載,代碼如下:
// 動態創建數組 String[] strs = (String[]) Array.newInstance(String.class, 8); // 創建一個多維數組 int[][] ints = (int[][]) Array.newInstance(int.class, 2, 3);
因為數組比較特殊,要想動態創建和訪問數組,基本的反射是無法實現的,“上帝對你關閉一扇門,同時會為你打開一扇窗。”,於是Java就專門定義了一個Array數組反射工具類來實現動態探知數組的功能。
注意:通過反射操作數組使用Array類,不要采用通用的反射處理API。