編寫高質量程式碼:改善Java程式的151個建議(第7章:泛型和反射___建議102~105)

阿赫瓦里發表於2016-10-09

建議102:適時選擇getDeclaredXXX和getXXX

  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,之後持續遞迴即可。

建議103:反射訪問屬性或方法時將Accessible設定為true

  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,這並不僅僅是因為操作習慣的問題,還是為我們的系統效能考慮。

 

 建議104:使用forName動態載入類檔案

  動態載入(Dynamic Loading)是指在程式執行時載入需要的類庫檔案,對Java程式來說,一般情況下,一個類檔案在啟動時或首次初始化時會被載入到記憶體中,而反射則可以在執行時再決定是否需要載入一個類,比如從Web上接收一個String引數作為類名,然後在JVM中載入並初始化,這就是動態載入,此動態載入通常是通過Class.forName(String)實現的,只是這個forName方法到底是什麼意思呢?

  我們知道一個類檔案只有在被載入到記憶體中才可能生成例項物件,也就是說一個物件的生成必然會經過兩個步驟:

  • 載入到記憶體中生成Class的例項物件
  • 通過new關鍵字生成例項物件

   如果我們使用的是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只是載入類,並不執行任何程式碼。

建議105:動態載入不適合陣列

 上一個建議解釋了為什麼要用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。

相關文章