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

建議102:適時選擇getDeclaredXXX和getXXX

  Java的Class類提供了不少的getDeclaredXXX方法和getXXX方法,例如getDeclaredMethod和getMethod成對出現,getDeclaredConstructors和getConstructors也是成對出現,那這二者之間有什麼差異呢?看以下代碼:java

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() {
        }
    }
}

  此段代碼運行後輸出以下:mysql

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)方法,並且不受限於訪問權限。sql

  其它的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方法能夠被任何一個類訪問。咱們編寫一個客戶端類來檢查該方法是否能夠反射執行:ide

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。

相關文章
相關標籤/搜索