1. 程式人生 > 其它 >Java泛型簡介二

Java泛型簡介二

  1.泛型擦除的理解

  關於泛型,我們先看一個示例:

public class TypeErasure {

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


}

  它的結果是true,這是因為在程式執行時期,這個泛型引數已經被擦除,返回型別都是List.class。泛型資訊只存在於編譯階段,在進入JVM之前,該資訊即會被擦除掉。虛擬機器對泛型其實一無所知,所有的工作都是編譯器做的,編譯器把型別<T>視為Object,並進行了安全的強制轉型。大家可以將List<XXX>的程式碼進行編譯,而後將生成的class檔案進行反編譯,即可看到反編譯的檔案中已經沒有了型別資訊。

  為了匹配JDK1.5以下的版本,這裡必須要在進入JVM之前對這個型別進行擦除。想象一下,如果在老版本的ArrayList中存放了A,B,C三種類型,都以Object處理,執行時存和放都合理地判斷並處理了相應的邏輯,但JDK1.5在引入泛型後編譯後不對型別擦除,而是生產了新的型別(類似ArrayList@Object@...),這就意味著,這個泛型在進行編譯之後就變成了新的型別,老程式碼中的A,B,C是無法向上轉型為ArrayList@Object@...的。如果此時執行環境升級為JDK1.5,那麼老版本的程式碼就會報錯。此時,為了匹配新版的JDK,開發者們就不得不對原始碼做出更改,這樣的代價既不利於JDK的推廣,也是各個服務商所無法承受的。

  另外,編譯器會阻止一個實際上會變成覆寫的泛型方法定義。例如:

  2.萬用字元與邊界

  參考1:https://www.cnblogs.com/wxw7blog/p/7517343.html

  參考2:https://www.liaoxuefeng.com/wiki/1252599548343744/1265104600263968

  注意萬用字元並不能用來定義介面、類或者方法,而是用於針對已經定義好的泛型方法引數,進行一個靈活的類型範圍選取。即只能用在引數接收上,很多時候也會搭配extends ,super進行型別上下邊界的劃定。這裡先看幾個例子:

import java.util.List;

public
class GenericCommonTest<T> { private T param; public void commonParam(GenericCommonTest<?> commonParam) { System.out.println(commonParam.getClass().getName()); } public void commonExtendsParam(GenericCommonTest<? extends Number> commonExtendsParam) { System.out.println(commonExtendsParam.getClass().getName()); } public void commonSuperParam(GenericCommonTest<? super Number> commonSuperParam) { System.out.println(commonSuperParam.getClass().getName()); } public void commonParamList(List<?> commonParamList) { commonParamList.add(null); } public static void main(String[] args) { GenericCommonTest<?> numberGenericCommonTest = new GenericCommonTest<>(); numberGenericCommonTest.commonParam(numberGenericCommonTest); numberGenericCommonTest.commonParam(null); } }

  其中,“?”被稱為無邊界的萬用字元,它的作用是讓泛型能夠接受未知型別的資料。

  這裡引入一個問題,如果List<T>型的引數,被定義為List<Number>,那麼作為Number子類的Integer,可否使用List<Integer>進行引數傳遞呢?答案是不可以。因為List<Integer>不是List<Number>的子類。為了解決這個問題,我們可以把引數定義為List<? extends Number>。

  萬用字元的使用,增加了泛型的靈活性,可以在設定型別引數時,不必只指定一個型別,而是符合某個繼承關係的一組型別。要注意List<? extends Number>還是不能呼叫add或者類似含義的set方法,因為編譯器不知道到底裝入的是Integer還是Double(這裡說明一下,基本型別是不能作為型別引數放入泛型的)。唯一的,只有null例外。而函式返回的接收引數可以設定為Number,因為這裡所有填入的型別都是Number的子類,所以這個限制即方法引數簽名set(List<? extends Number> list)無法傳遞任何Number的子類給方法。同樣地原理,List<?>中的add只能加入null,此時即便是Object的加入也會編譯報錯。因為不確定List泛型是什麼型別的,所以任何型別的放入都是錯誤的,只有null可以放入。而List<?>的get方法的返回型別只能是Object,因為不知道里面放的什麼型別,而Object是所有型別的父類,可以作為接收引數。此時List<?>與List<? extends Object>含義是相同的。

  所以,基於以上的限制,方法引數型別List<? extends Number>表明了該方法內部只會讀取List內部的元素,不會修改List的元素。

  另外,在定義泛型類,介面時,<T extends Number>表示泛型型別限定為Number以及Number的子類。

  "? super E"表示下邊界。即傳入的型別,必須是E的父類或者本身。如List<? super Integer>,那麼傳入的型別可能為List<Object>,List<Integer>,List<Number>。可以看一個JDK中的例項:

    /**
     * Copies all of the elements from one list into another.  After the
     * operation, the index of each copied element in the destination list
     * will be identical to its index in the source list.  The destination
     * list must be at least as long as the source list.  If it is longer, the
     * remaining elements in the destination list are unaffected. <p>
     *
     * This method runs in linear time.
     *
     * @param  <T> the class of the objects in the lists
     * @param  dest The destination list.
     * @param  src The source list.
     * @throws IndexOutOfBoundsException if the destination list is too small
     *         to contain the entire source List.
     * @throws UnsupportedOperationException if the destination list's
     *         list-iterator does not support the <tt>set</tt> operation.
     */
    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        int srcSize = src.size();
        if (srcSize > dest.size())
            throw new IndexOutOfBoundsException("Source does not fit in dest");

        if (srcSize < COPY_THRESHOLD ||
            (src instanceof RandomAccess && dest instanceof RandomAccess)) {
            for (int i=0; i<srcSize; i++)
                dest.set(i, src.get(i));
        } else {
            ListIterator<? super T> di=dest.listIterator();
            ListIterator<? extends T> si=src.listIterator();
            for (int i=0; i<srcSize; i++) {
                di.next();
                di.set(si.next());
            }
        }
    }

  類比List<? extends Number>,使用super允許set<? super Integer>方法傳入Integer的引用,但是無法通過get獲取。唯一例外是可以get到Object型別。對比結論:

<? extends T>允許呼叫讀方法T get()獲取T的引用,但不允許呼叫寫方法set(T)傳入T的引用(傳入null除外);
<? super T>允許呼叫寫方法set(T)傳入T的引用,但不允許呼叫讀方法T get()獲取T的引用(獲取Object除外)。

  對比上面的copy示例,完美展示了兩個邊界使用的意圖。

  OEIS原則:需要獲取T,對方法來說,就是要OUT,使用extends;如果要寫入T,那麼就是IN,使用super。(PECS還是會記混)。

  無限定萬用字元<?>既不允許set(null除外),也不允許get(Object除外),所以使用場景比較少。大多數情況下,都可以使用T對?進行消除。?一般都是搭配extends或者super來使用。