Effective Java 第三版——65. 接口優於反射

Tips
書中的源代碼地址:https://github.com/jbloch/effective-java-3e-source-code
注意,書中的有些代碼裏方法是基於Java 9 API中的,因此JDK 最好下載 JDK 9以上的版本。java

Effective Java, Third Edition

65. 接口優於反射

核心反射工具java.lang.reflect提供對任意類的編程訪問。 給定一個Class對象,能夠得到Constructor,Method和Field實例,這些實例表示由Class實例表示的類的構造方法,方法和屬性。 這些對象提供對類的成員名稱,屬性類型,方法簽名等的編程訪問。git

此外,Constructor,Method和Field實例容許反射性地操做它們的底層對應物:能夠經過在Constructor,Method和Field實例上調用方法來構造實例,調用方法和訪問底層類的屬性。 例如,Method.invoke方法容許在任何類的任何對象上調用任何方法(受一般的安全性約束)。 反射容許一個類使用另外一個類,即便在編譯前者時後者類並不存在。 然而,這種能力是有代價的:github

  • 失去了編譯時類型檢查的全部好處,包括異常檢查。若是一個程序試圖經過反射調用一個不存在的或不可訪問的方法,會在運行時失敗,除非採起了特殊的預防措施。編程

  • 執行反射訪問所需的代碼笨拙而冗長。寫起來很乏味,讀起來很困難。安全

  • 性能受損。反射方法調用比普通方法調用慢得多。到底慢了多少還很難說,由於有不少因素在起做用。在個人機器上,調用一個沒有輸入參數和返回int類型的方法時,反射方法執行要比普通方法慢11倍。框架

有一些複雜的應用程序須要反射。示例包括代碼分析工具和依賴注入框架。即便是這樣的工具,隨着它的缺點變得愈來愈明顯,也在逐漸遠離反射。若是你對應用程序是否須要反射有任何疑問,那麼它多是不須要的。less

經過以很是有限的形式使用反射,能夠得到反射的許多好處,同時花費不多。對於許多必須使用在編譯時不可用的類的程序,在編譯時存在一個適當的接口或父類來引用該類( 條目64)。若是是這種狀況,可使用反射建立實例,並經過它們的接口或父類正常地訪問它們ide

例如,這是一個建立Set<String>實例的程序,其實例的類由第一個命令行參數指定。 程序將剩餘的命令行參數插入到集合中並打印它。 不管第一個參數如何,程序都會打印剩餘的參數,並刪除重複項。 可是,打印這些參數的順序取決於第一個參數中指定的類。 若是指定java.util.HashSet,則它們以明顯隨機的順序打印; 若是指定java.util.TreeSet,則它們按字母順序打印,由於TreeSet中的元素是按順序排序的:函數

// Reflective instantiation with interface access
public static void main(String[] args) {
    // Translate the class name into a Class object
    Class<? extends Set<String>> cl = null;
    try {
        cl = (Class<? extends Set<String>>)  // Unchecked cast!
                Class.forName(args[0]);
    } catch (ClassNotFoundException e) {
        fatalError("Class not found.");
    }
    // Get the constructor
    Constructor<? extends Set<String>> cons = null;
    try {
        cons = cl.getDeclaredConstructor();
    } catch (NoSuchMethodException e) {
        fatalError("No parameterless constructor");
    }

    // Instantiate the set
    Set<String> s = null;
    try {
        s = cons.newInstance();
    } catch (IllegalAccessException e) {
        fatalError("Constructor not accessible");
    } catch (InstantiationException e) {
        fatalError("Class not instantiable.");
    } catch (InvocationTargetException e) {
        fatalError("Constructor threw " + e.getCause());
    } catch (ClassCastException e) {
        fatalError("Class doesn't implement Set");
    }

    // Exercise the set
    s.addAll(Arrays.asList(args).subList(1, args.length));
    System.out.println(s);
}

private static void fatalError(String msg) {
    System.err.println(msg);
    System.exit(1);
}

雖然這只是一個演示程序,但它演示的技術很是強大的。演示程序能夠很容易地變成泛型集合測試程序,經過積極地操縱一個或多個實例並檢查它們是否遵照Set約定來驗證指定的Set實現。 一樣,它能夠變成泛型集合性能分析工具。 事實上,這種技術足以實現一個成熟的服務提供者框架(service provider framework)(條目 1)。 一般,這種技術就是你在反射中所須要的。工具

這個例子說明了反射的兩個缺點。 首先,該示例在運行時生成六個不一樣的異常,若是不使用反射實例化,則全部這些異常都是編譯時錯誤。 (爲了好玩,能夠經過傳入適當的命令行參數使程序生成六個異常中的每個)。第二個缺點是須要25行繁瑣的代碼才能從其名稱生成類的實例, 而構造函數方法使用一行代碼便可。 能夠經過捕獲ReflectiveOperationException來減小程序的長度,該異常是Java 7中引入的各類反射異常的父類。這兩個缺點僅限於實例化對象的程序部分。 實例化後,該集合與任何其餘Set實例沒法區分。 在真實的程序中,大量的代碼所以不會受這種限定的反射使用的影響。

若是編譯此程序,會得到未經檢查的強制轉換警告。 這個警告是合法的, 所以轉換Class<? extends Set<String>> 也會成功,即便指定的集合不是Set接口的實現。在這種狀況下,程序在實例化類時拋出ClassCastException異常。 要了解有關抑制警告的信息,請閱讀條目 27。

反射的合法(若是不多)用途是管理類對運行時可能不存在的其餘類、方法或屬性的依賴關係。若是你正在編寫一個必須針對其餘包的多個版本運行的包,這將很是有用。該技術是根據支持包所需的最小環境(一般是最老的版本)編譯包,並反射訪問任何較新的類或方法。要使此工做正常進行,若是試圖訪問的新類或方法在運行時不存在,則必須採起適當的操做。適當的行動可能包括使用一些替代方法來完成相同的目標,或者使用簡化的功能進行操做。

總之,反射是一種功能強大的工具,對於某些複雜的系統編程任務是必需的,可是它有不少缺點。若是編寫的程序必須在編譯時處理未知的類,則應該儘量只使用反射實例化對象,並使用在編譯時已知的接口或父類訪問對象。

相關文章
相關標籤/搜索