1. 程式人生 > >編寫高質量程式碼:改善Java程式的151個建議(第7章:泛型和反射___建議93~97)

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

我們最大的弱點在於放棄。成功的必然之路就是不斷的重來一次。 --達爾文

建議93:Java的泛型是可以擦除的

建議94:不能初始化泛型引數和陣列

建議95:強制宣告泛型的實際型別

建議96:不同的場景使用不同的泛型萬用字元

建議97:警惕泛型是不能協變和逆變的

泛型可以減少將至型別轉換,可以規範集合的元素型別,還可以提高程式碼的安全性和可讀性,優先使用泛型。

反射可以“看透”程式的執行情況,可以讓我們在執行期知曉一個類或例項的執行情況,可以動態的載入和呼叫,雖然有一定的效能憂患,但它帶給我們的便利大於其效能缺陷。

建議93:Java的泛型是可以擦除的

1、Java泛型的引入加強了引數型別的安全性,減少了型別的轉換,Java的泛型在編譯器有效,在執行期被刪除,也就是說所有的泛型引數型別在編譯後會被清除掉,我們來看一個例子,程式碼如下:

兩個一樣的方法衝突了?

這就是Java泛型擦除引起的問題:在編譯後所有的泛型型別都會做相應的轉化。轉換規則如下:

  • List<String>、List<Integer>、List<T>擦除後的型別為List
  • List<String>[] 擦除後的型別為List[].
  • List<? extends E> 、List<? super E> 擦除後的型別為List<E>.
  • List<T extends Serializable & Cloneable >擦除後的型別為List< Serializable>.

2、明白了這些規則,再看如下程式碼:

public static void main(String[] args) {
    List<String> list = new ArrayList<String>();
    list.add("abc");
    String str = list.get(0);
}

 進過編譯後的擦除處理,上面的程式碼和下面的程式時一致的:

public static void main(String[] args) {
    List list = new ArrayList();
    list.add("abc");
    String str = (String) list.get(0);
}

3、Java之所以如此處理,有兩個原因:

① 避免JVM的執行負擔。

如果JVM把泛型型別延續到執行期,那麼JVM就需要進行大量的重構工作了。

② 版本相容

在編譯期擦除可以更好的支援原生型別(Raw Type),在Java1.5或1.6...平臺上,即使宣告一個List這樣的原生型別也是可以正常編譯通過的,只是會產生警告資訊而已。

4、明白了Java泛型是型別擦除的,我們就可以解釋類似如下的問題了:

① 泛型的class物件是相同的:每個類都有一個class屬性,泛型化不會改變class屬性的返回值,例如:

public static void main(String[] args) {
    List<String> list = new ArrayList<String>();
    List<Integer> list2 = new ArrayList<Integer>();
    System.out.println(list.getClass());
    System.out.println(list.getClass()==list2.getClass());
}

以上程式碼返回true,原因很簡單,List<String>和List<Integer>擦除後的型別都是List,沒有任何區別。

② 泛型陣列初始化時不能宣告泛型,如下程式碼編譯時通不過: 

List<String>[] listArray = new List<String>[];

原因很簡單,可以宣告一個帶有泛型引數的陣列,但不能初始化該陣列,因為執行了型別擦除操作,List<Object>[]與List<String>[] 就是同一回事了,編譯器拒絕如此宣告。

③ instanceof不允許存在泛型引數

以下程式碼不能通過編譯,原因一樣,泛型型別被擦除了:

建議94:不能初始化泛型引數和陣列

泛型型別在編譯期被擦除,我們在類初始化時將無法獲得泛型的具體引數,比如這樣的程式碼:

這段程式碼是編譯不過的,因為編譯時需要獲得T型別,但泛型在編譯期型別已經被擦除了。在某些情況下,我們需要泛型陣列,那該如何處理呢?程式碼如下:

public class Student<T> {
    // 不再初始化,由建構函式初始化
    private T t;
    private T[] tArray;
    private List<T> list = new ArrayList<T>();

    // 建構函式初始化
    public Student() {
        try {
            Class<?> tType = Class.forName("");
            t = (T) tType.newInstance();
            tArray = (T[]) Array.newInstance(tType, 5);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

此時,執行就沒有什麼問題了,剩下的問題就是怎麼在執行期獲得T的型別,也就是tType引數,一般情況下泛型型別是無法獲取的,不過,在客戶端呼叫時多傳輸一個T型別的class就會解決問題。

類的成員變數是在類初始化前初始化的,所以要求在初始化前它必須具有明確的型別,否則就只能宣告,不能初始化。

建議95:強制宣告泛型的實際型別

Arrays工具類有一個方法asList可以把一個邊長引數或陣列轉變為列表,但它有一個缺點:它所生成的list長度是不可變的,而在我們的專案開發中有時會很不方便。如果期望可變,那就需要寫一個數組的工具類了,程式碼如下:

class ArrayUtils {
    // 把一個變長引數轉化為列表,並且長度可變
    public static <T> List<T> asList(T... t) {
        List<T> list = new ArrayList<T>();
        Collections.addAll(list, t);
        return list;
    }
}

這很簡單,與Arrays.asList的呼叫方式相同,我們傳入一個泛型物件,然後返回相應的List,程式碼如下:

public static void main(String[] args) {
    // 正常用法
    List<String> list1 = ArrayUtils.asList("A", "B");
    // 引數為空
    List list2 = ArrayUtils.asList();
    // 引數為整型和浮點型的混合
    List list3 = ArrayUtils.asList(1, 2, 3.1);
}

這裡有三個變數需要說明:

1、變數list1:變數list1是一個常規用法,沒有任何問題,泛型實際引數型別是String,返回結果就是一個容納String元素的List物件。

2、變數list2:變數list2它容納的是什麼元素呢?我們無法從程式碼中推斷出list2列表到底容納的是什麼元素(因為它傳遞的引數是空,編譯器也不知道泛型的實際引數型別是什麼),不過,編譯器會很聰明地推斷出最頂層類Object就是其泛型型別,也就是說list2的完整定義如下:

List<Object> list2 = ArrayUtils.asList();

如此一來,編譯器就不會給出" unchecked "警告了。現在新的問題又出現了:如果期望list2是一個Integer型別的列表,而不是Object列表,因為後續的邏輯會把Integer型別加入到list2中,那該如何處理呢?

強制型別轉換(把asList強制轉換成List<Integer>)?行不通,雖然Java泛型是編譯期擦出的,但是List<Object>和List<Integer>沒有繼承關係,不能強制轉換。  

重新宣告一個List<Integer>,然後讀取List<Object>元素,一個一個地向下轉型過去?麻煩,而且效率又低。

最好的解決辦法是強制宣告泛型型別,程式碼如下:

List<Integer> intList = ArrayUtils.<Integer>asList();

就這麼簡單,asList方法要求的是一個泛型引數,那我們就在輸入前定義這是一個Integer型別的引數,當然,輸出也是Integer型別的集合了。

3、變數list3:變數list3有兩種型別的元素:整數型別和浮點型別,那它生成的List泛型化引數應該是什麼呢?是Integer和Float的父類Number?你太高看編譯器了,它不會如此推斷的,當它發現多個元素的實際型別不一致時就會直接確認泛型型別是Object,而不會去追索元素的公共父類是什麼,但是對於list3,我們更期望它的泛型引數是Number,都是數字嘛,參照list2變數,程式碼修改如下:

List<Number> list3 = ArrayUtils.<Number>asList(1, 2, 3.1);

Number是Integer和Float的父類,先把三個輸入引數、輸出引數同類型,問題是我們要在什麼時候明確泛型型別呢?一句話:無法從程式碼中推斷出泛型的情況下,即可強制宣告泛型型別。

建議96:不同的場景使用不同的泛型萬用字元

Java泛型支援萬用字元(Wildcard),可以單獨使用一個“?”表示任意類,也可以使用extends關鍵字表示某一個類(介面)的子型別,還可以使用super關鍵字表示某一個類(介面)的父型別,但問題是什麼時候該用extends,什麼該用super呢?

1、泛型結構只參與 “讀” 操作則限定上界(extends關鍵字),也就是要界定泛型的上界

編譯失敗,失敗的原因是list中的元素型別不確定,也就是編譯器無法推斷出泛型型別到底是什麼,是Integer型別?是Double?還是Byte?這些都符合extends關鍵字的定義,由於無法確定實際的泛型型別,所以編譯器拒絕了此類操作。

2、泛型結構只參與“寫” 操作則限定下界(使用super關鍵字),也就是要界定泛型的下界

甭管它是Integer的123,還是浮點數3.14,都可以加入到list列表中,因為它們都是Number的型別,這就保證了泛型類的可靠性。

建議97:警惕泛型是不能協變和逆變的

協變:窄型別替換寬型別

逆變:寬型別替換窄型別

1、泛型不支援協變,編譯不通過,,,窄型別變成寬型別(Integer>>Number)

泛型不支援協變,但可以使用萬用字元模擬協變,程式碼如下:

" ? extends Number " 表示的意思是,允許Number的所有子類(包括自身) 作為泛型引數型別,但在執行期只能是一個具體型別,或者是Integer型別,或者是Double型別,或者是Number型別,也就是說萬用字元只在編碼期有效,執行期則必須是一個確定的型別。

2、泛型不支援逆變

" ? super Integer " 的意思是可以把所有的Integer父型別(自身、父類或介面) 作為泛型引數,這裡看著就像是把一個Number型別的ArrayList賦值給了Integer型別的List,其外觀類似於使用一個寬型別覆蓋一個窄型別,它模擬了逆變的實現。

 

編寫高質量程式碼:改善Java程式的15