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

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

  泛型可以減少強制型別的轉換,可以規範集合的元素型別,還可以提高程式碼的安全性和可讀性,正式因為有這些優點,自從Java引入泛型後,專案的編碼規則上便多了一條:優先使用泛型。

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

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

  Java泛型(Generic) 的引入加強了引數型別的安全性,減少了型別的轉換,它與C++中的模板(Temeplates) 比較類似,但是有一點不同的是:Java的泛型在編譯器有效,在執行期被刪除,也就是說所有的泛型引數型別在編譯後會被清除掉,我們來看一個例子,程式碼如下:

 1 public class Foo {
 2     //arrayMethod接收陣列引數,並進行過載
 3     public void arrayMethod(String[] intArray) {
 4 
 5     }
 6 
 7     public void arrayMethod(Integer[] intArray) {
 8 
 9     }
10     //listMethod接收泛型List引數,並進行過載
11     public void listMethod(List<String> stringList) {
12 
13     }
14     public void listMethod(List<Integer> intList) {
15         
16     }
17 }

  程式很簡單,編寫了4個方法,arrayMethod方法接收String陣列和Integer陣列,這是一個典型的過載,listMethod接收元素型別為String和Integer的list變數。現在的問題是,這段程式是否能編譯?如果不能?問題出在什麼地方?

  事實上,這段程式時無法編譯的,編譯時報錯資訊如下:

  

  這段錯誤的意思:簡單的的說就是方法簽名重複,其實就是說listMethod(List<Integer> intList)方法在編譯時擦除型別後是listMethod(List<E> intList)與另一個方法重複。這就是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>.

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

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);
    }

  Java編譯後位元組碼中已經沒有泛型的任何資訊了,也就是說一個泛型類和一個普通類在經過編譯後都指向了同一位元組碼,比如Foo<T>類,經過編譯後將只有一份Foo.class類,不管是Foo<String>還是Foo<Integer>引用的都是同一位元組碼。Java之所以如此處理,有兩個原因:

  • 避免JVM的大換血。C++泛型生命期延續到了執行期,而Java是在編譯期擦除掉的,我們想想,如果JVM也把泛型型別延續到執行期,那麼JVM就需要進行大量的重構工作了。
  • 版本相容:在編譯期擦除可以更好的支援原生型別(Raw Type),在Java1.5或1.6...平臺上,即使宣告一個List這樣的原生型別也是可以正常編譯通過的,只是會產生警告資訊而已。

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

  1. 泛型的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()==list2.getClass());
    }

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

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

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

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

  3.instanceof不允許存在泛型引數

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

    List<String> list = new ArrayList<String>();
    System.out.println(list instanceof List<String>);

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

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

class Test<T> {
    private T t = new T();
    private T[] tArray = new T[5];
    private List<T> list = new ArrayList<T>();
}

  這段程式碼有神麼問題呢?t、tArray、list都是類變數,都是通過new宣告瞭一個型別,看起來非常相似啊!但這段程式碼是編譯不過的,因為編譯器在編譯時需要獲得T型別,但泛型在編譯期型別已經被擦除了,所有new T()和 new T[5]都會報錯(有人可能會有疑問,泛型型別可以擦除為頂級Object,那T型別擦除成Object不就可以編譯了嗎?這樣也不行,泛型只是Java語言的一部分,Java語言畢竟是一個強型別、編譯型的安全語言,要確保執行期的穩定性和安全性就必須要求在編譯器上嚴格檢查)。可為什麼new ArrayList<T>()卻不會報錯呢?

  這是因為ArrayList表面是泛型,其實已經在編譯期轉為Object了,我們來看一下ArrayList的原始碼就清楚了,程式碼如下: 

 1 public class ArrayList<E> extends AbstractList<E> implements List<E>,
 2         RandomAccess, Cloneable, java.io.Serializable {
 3     // 容納元素的陣列
 4     private transient Object[] elementData;
 5 
 6     // 建構函式
 7     public ArrayList() {
 8         this(10);
 9     }
10 
11     // 獲得一個元素
12     public E get(int index) {
13         rangeCheck(index);
14         // 返回前強制型別轉換
15         return elementData(index);
16     }
17     /* 其它程式碼略 */
18 
19 }

  注意看elementData的定義,它容納了ArrayList的所有元素,其型別是Object陣列,因為Object是所有類的父類,陣列又允許協變(Covariant),因此elementData陣列可以容納所有的例項物件。元素加入時向上轉型為Object型別(E型別轉換為Object),取出時向下轉型為E型別,如此處理而已。

  在某些情況下,我們需要泛型陣列,那該如何處理呢?程式碼如下:

 1 class Test<T> {
 2     // 不再初始化,由建構函式初始化
 3     private T t;
 4     private T[] tArray;
 5     private List<T> list = new ArrayList<T>();
 6 
 7     // 建構函式初始化
 8     public Test() {
 9         try {
10             Class<?> tType = Class.forName("");
11             t = (T) tType.newInstance();
12             tArray = (T[]) Array.newInstance(tType, 5);
13         } catch (Exception e) {
14             e.printStackTrace();
15         }
16     }
17 }

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

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

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

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

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

  這很簡單,與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關鍵字)

  閱讀如下程式碼,想想看我們的業務邏輯操作是否還能繼續:

public static <E> void read(List<? super E> list) {
        for (Object obj : list) {
            // 業務邏輯操作
        }
    }

  從List列表中讀取元素的操作(比如一個數字列表中的求和計算),你覺得方法read能繼續寫下去嗎?

  答案是:不能,我們不知道list到底存放的是什麼元素,只能推斷出E型別是父類,但問題是E型別的父類又是什麼呢?無法再推斷,只有執行期才知道,那麼編碼器就無法操作了。當然,你可以把它當做是Object類來處理,需要時再轉換成E型別---這完全違背了泛型的初衷。在這種情況下,“讀” 操作如果期望從List集合中讀取資料就需要使用extends關鍵字了,也就是要界定泛型的上界,程式碼如下:

public static <E> void read(List<? extends E> list) {
        for (E e : list) {
            // 業務邏輯操作
        }
    }

  此時,已經推斷出List集合中取出的元素時E型別的元素。具體是什麼型別的元素就要等到執行期才確定了,但它一定是一個確定的型別,比如read(Arrays.asList("A"))呼叫該方法時,可以推斷出List中的元素型別是String,之後就可以對List中的元素進行操作了。如加入到另外的List<E>中,或者作為Map<E,V>的鍵等。

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

  先看如下程式碼能否編譯:

public static <E> void write(List<? extends Number> list){
        //加入一個元素
        list.add(123);
    }

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

  在此種情況下,只有一個元素時可以add進去的:null值,這是因為null是一個萬用型別,它可以是所有類的例項物件,所以可以加入到任何列表中。

  Object是否可以?不可以,因為它不是Number子類,而且即使把List變數修改為List<? extends Object> 型別也不能加入,原因很簡單,編譯器無法推斷出泛型型別,加什麼元素都是無效的。

  在這種“寫”的操作的情況下,使用super關鍵字限定泛型的下界才是正道,程式碼如下:

public static <E> void write(List<? super Number> list){
        //加入元素
        list.add(123);
        list.add(3.14);
    }

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

  對於是要限定上界還是限定下界,JDK的Collections.copy方法是一個非常好的例子,它實現了把源列表的所有元素拷貝到目標列表中對應的索引位置上,程式碼如下:

 1     public static <T> void copy(List<? super T> dest, List<? extends T> src) {
 2         int srcSize = src.size();
 3         if (srcSize > dest.size())
 4             throw new IndexOutOfBoundsException("Source does not fit in dest");
 5 
 6         if (srcSize < COPY_THRESHOLD ||
 7             (src instanceof RandomAccess && dest instanceof RandomAccess)) {
 8             for (int i=0; i<srcSize; i++)
 9                 dest.set(i, src.get(i));
10         } else {
11             ListIterator<? super T> di=dest.listIterator();
12             ListIterator<? extends T> si=src.listIterator();
13             for (int i=0; i<srcSize; i++) {
14                 di.next();
15                 di.set(si.next());
16             }
17         }
18     }

  源列表是用來提供資料的,所以src變數需要界定上界,要有extends關鍵字。目標列表是用來寫資料的,所以dest變數需要界定下界,帶有super關鍵字。

  如果一個泛型結構既用作 “讀” 操作又用作“寫操作”,那該如何進行限定呢?不限定,使用確定的泛型型別即可,如List<E>.

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

  什麼叫協變和逆變?

  在程式語言的型別框架中,協變和逆變是指寬型別和窄型別在某種情況下(如引數、泛型、返回值)替換或交換的特性,簡單的說,協變是一個窄型別替換寬型別,而逆變則是用寬型別覆蓋窄型別。其實,在Java中協變和逆變我們已經用了很久了,只是我們沒發覺而已,看如下程式碼:

class Base {
    public Number doStuff() {
        return 0;
    }
}

class Sub extends Base {
    @Override
    public Integer doStuff() {
        return 0;
    }
}

  子類的doStuff方法返回值的型別比父類方法要窄,此時doStuff方法就是一個協變方法,同時根據Java的覆寫定義來看,這又屬於覆寫。那逆變是怎麼回事呢?程式碼如下: 

class Base {
    public void doStuff(Integer i) {
        
    }
}

class Sub extends Base {
    @Override
    public void doStuff(Number n) {
      
    }
}

   子類的doStuff方法的引數型別比父類要寬,此時就是一個逆變方法,子類擴大了父類方法的輸入引數,但根據覆寫的定義來看,doStuff不屬於覆寫,只是過載而已。由於此時的doStuff方法已經與父類沒有任何關係了,只是子類獨立擴充套件出的一個行為,所以是否宣告為doStuff方法名意義不大,逆變已經不具有特別的意義了,我們重點關注一下協變,先看如下程式碼是否是協變:

    public static void main(String[] args) {
        Base base = new Sub();
    }

  base變數是否發生了協變?是的,發生了協變,base變數是Base型別,它是父類,而其賦值卻是在子類例項,也就是用窄型別覆蓋了寬型別。這也叫多型,兩者同含義。

  說了這麼多,下面再再來想想泛型是否支援協變和逆變呢,答案是:泛型既不支援協變,也不支援逆變。為什麼會不支援呢?

(1)、泛型不支援協變:陣列和泛型很相似,一個是中括號,一個是尖括號,那我們就以陣列為參照物件,看如下程式碼:

    public static void main(String[] args) {
        //陣列支援協變
        Number [] n = new Integer[10];
        //編譯不通過,泛型不支援協變
        List<Number> list = new ArrayList<Integer>();
    }

  ArrayList是List的子型別,Integer是Number的子型別,里氏替換原則在此行不通了,原因就是Java為了保證執行期的安全性,必須保證泛型引數的型別是固定的,所以它不允許一個泛型引數可以同時包含兩種型別,即使是父子類關係也不行。

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

        //Number子型別(包括Number型別) 都可以是泛型引數型別
        List<? extends Number> list = new ArrayList<Integer>();

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

(2)、泛型不支援逆變

  java雖然允許逆變存在,但在對型別賦值上是不允許逆變的,你不能把一個父類例項物件賦給一個子類型別變數,泛型自然也不允許此種情況發生了。但是它可以使用super關鍵字來模擬實現,程式碼如下:

        //Integer的父型別(包括Integer)都可以是泛型引數型別
        List<? super Integer> list = new ArrayList<Number>();

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

  泛型既不支援協變,也不支援逆變,帶有泛型引數的子型別定義與我們經常使用的類型別也不相同,其基本型別關係如下表所示:

泛型萬用字元QA
Integer是Number的子型別? 正確
ArrayList<Integer> 是List<Integer> 的子型別? 正確
Integer[]是 Number[]的子型別? 正確
List<Integer> 是 List<Number> 的子型別? 錯誤
List<Integer> 是 List<? extends  Integer> 的子型別? 錯誤
List<Integer> 是 List<? super  Integer> 的子型別? 錯誤
                                                     Java的泛型是不支援協變和逆變的,只是能夠實現逆變和協變

相關文章