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

  泛型能夠減小強制類型的轉換,能夠規範集合的元素類型,還能夠提升代碼的安全性和可讀性,正式由於有這些優勢,自從Java引入泛型後,項目的編碼規則上便多了一條:優先使用泛型。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泛型擦除引發的問題:在編譯後全部的泛型類型都會作相應的轉化。轉換規則以下:dom

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

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

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的泛型是不支持協變和逆變的,只是可以實現逆變和協變
相關文章
相關標籤/搜索