泛型中 ? super T 和 ? extends T 的區別

泛型中 ? super T? extends T 的區別

概要

  1. 類型擦除
  2. ? 的用法
  3. extends 的用法
  4. super的用法

類型擦除

import java.util.*;
public class main{
 public static void main(String[] args){
   Class c1 = new ArrayList<String>().getClass();
   Class c2 = new ArrayList<Integer>().getClass();
   System.out.println(c1 == c2);
 }
}

上面程序運行的結果是true。出現這個結果的原因正是類型擦除。

在Java語言中,泛型是通過類型擦除來實現的,這意味着當你在使用泛型時,任何具體的類型都將被擦除,你唯一知道的就是你正在使用的是一個對象。因此上面List 和List在運行時事實上是同樣的類型。(泛型類型只有在靜態類型檢查期間纔出現,靜態類型檢查:基於程序的源代碼來驗證類型安全的過程;動態類型檢查:在程序運行期間驗證類型安全的過程;)

擦除帶來的問題

class HasF{
 public void f(){
   System.out.println("f");
 }
}
class Manipulator<T>{
 private T obj;
 public Manipulator(T x){
   obj = x;
 }
 public void manipulate(){
   obj.f();
 }
}
public class main{

 public static  void main(String[] args){
   HasF hasf = new HasF();
   Manipulator<HasF> m = new Manipulator<HasF>(hasf);
   m.manipulate()  // error
 }
}

上面這段代碼會報錯,正是由於類型擦除導致的。因爲類型擦除,所以泛型內部無法知道類型的信息,當你調用object沒有的方法時,就會報錯

補充一點:邊界

因爲擦除在方法體中移除了類型信息,所以運行時,你就需要辨別邊界,邊界是指:對象進入和離開方法的地點,這裏是指泛型的方法體。

public class ArrayMaker<T> {
 private Class<T> kind;
 public ArrayMaker(Class<T> kind) { this.kind = kind; }
 @SuppressWarnings("unchecked")
 T[] create(int size) {
   return (T[])Array.newInstance(kind, size);
 }
 public static void main(String[] args) {
   ArrayMaker<String> stringMaker =
     new ArrayMaker<String>(String.class);
   String[] stringArray = stringMaker.create(9);
   System.out.println(Arrays.toString(stringArray));
 }
} /* Output:
[null, null, null, null, null, null, null, null, null]
*///:~

上面這段代碼就是因爲在泛型的方法體中初始化,所以就把所有的類型當成了Object類型,因此輸出的結果都是null。

仔細查看下面倆段代碼:

// 不存在泛型
public class SimpleHolder {
 private Object obj;
 public void set(Object obj) { this.obj = obj; }
 public Object get() { return obj; }
 public static void main(String[] args) {
   SimpleHolder holder = new SimpleHolder();
   holder.set("Item");
   String s = (String)holder.get();
 }
} ///:~
/// 存在泛型
public class GenericHolder<T> {
 private T obj;
 public void set(T obj) { this.obj = obj; }
 public T get() { return obj; }
 public static void main(String[] args) {
   GenericHolder<String> holder =
     new GenericHolder<String>();
   holder.set("Item");
   String s = holder.get();
 }
} 

但是如果你仔細查看倆段代碼的字節碼,你會發現是相同的。並且你會發現泛型所有的動作都是發生在邊界處,會對傳進來的值進行額外的編譯檢查,並插入對傳遞出去的值的類型。所以,可以得出一個結論,邊界就是發生動作的地方。

? 的用法

下面是使用案例

public class UnboundedWildcardsl {
   static List list1;
   static List<?> list2;
   static List<? extends Object> list3;
//    @SuppressWarnings("unchecked")
   static void assign1(List list){
       list1 = list;
       list2 = list;

       list3 = list;     //未檢查的轉換
   }
   static void assign2(List<?> list){
       list1 = list;
       list2 = list;
       list3 = list;
   }
   static void assign3(List<? extends Object> list){
       list1 = list;
       list2 = list;
       list3 = list;
   }

   public static void main(String[] args) {
       assign1(new ArrayList());
       assign2(new ArrayList());
       assign3(new ArrayList());

       assign1(new ArrayList<>());
       assign2(new ArrayList<>());
       assign3(new ArrayList<>());

       List<?> wildList = new ArrayList();
       wildList = new ArrayList<String>();
       assign1(wildList);
       assign2(wildList);
       assign3(wildList);
   }
}

但是如果你仔細查看倆段代碼的字節碼,你會發現是相同的。並且你會發現泛型所有的動作都是發生在邊界處,會對傳進來的值進行額外的編譯檢查,並插入對傳遞出去的值的類型。所以,可以得出一個結論,邊界就是發生動作的地方。

extends 的用法

下面展示的是如何使用:以 List爲例來展示使用:

    //Number extends Number 
    List<? extends Number>  foo3 = new ArrayList<? extends Number>();

    //Integer extends Number
    List<? extends Number> foo3 = new ArrayList<? extends Integer>();

    //Double extends Number
    List<? extends Number> foo3 = new ArrayList<? extends Double>();

    //下面的是錯誤使用
    foo3.add(12);
  1. 讀取操作:通過以上給定的賦值語句,你一定能從foo3列表中讀取到的元素類型是什麼呢?你可以讀取到Number,因爲以上的列表要麼包含Number的子類。你不能保證讀取到Integer,因爲foo3可能指向的是List。你不能保證讀取到時DOuble,因爲foo3可能指向的是List。
  2. 寫入操作:通過以上給定的賦值語句,你能把一個什麼類型的元素合法地插入到foo3中呢?必不能插入一個Integer元素,因爲foo3可能指向List。你不能插入一個Double元素,因爲foo3可能指向List。你不能插入一個Number元素,因爲foo3可能指向List。因此你不能網List<? extends T>中插入任何類型的對象。因爲你不能保證列表的實際指向的類型是什麼,你並不能保證列表中實際存儲什麼類型的對象。你唯一可保證的是,你可從中讀取到T或者T的子類。

super 的用法

下面以List<? super Integer>來展示super的用法

//integer is a superClass of Integer 
List<? super Integer>  foo3 = new ArrayList<Integer>()

//Number is a superClass of Integer
List<? super Integer> foo3 = new ArrayList<Number>();

//Object is superClass of Integer
List<? super Integer> foo3 = new ArrayList<Object>();

//下面代碼是錯誤的
list.get(index);  //不能獲取
  1. 讀取操作:通過以上給定的賦值語句,你一定能從foo3列表中讀取到的元素的類型是什麼呢?你不能保證讀取到Integer,因爲foo3可能指向List或者List。你不能保證讀取到Number,因爲foo3可能指向List。唯一可以保證的是,你讀取到是Object或者Object的子類對象。但是你並不知道子類是什麼。
  2. 寫入操作:通過以上給定的賦值語句,你能把一個什麼類型的元素插入到foo3中呢?你可以插入Integer對象,因爲上述聲明的列表都支持Integer對象。你可以插入Integer的子類的對象,因爲Integer的子類同時也是Integer,上述聲明列表都支持Integer對象。你不能插入Number對象,因爲foo3可能指向ArrayList。你不能插入Object對象,因爲foo3可能指向ArrayList。

PECS(Pruducer extends,Consumer super)

請記住PECS原則:生產者(Producer)使用extends,消費者(Consumer)使用super。

  • 生產者使用extends

如果你需要一個列表提供T類型的元素(即你想從列表中讀取T類型的元素),你需要把這個列表聲明成《? extends T》,比如List 《? extends Integer》,因此你不能往該列表中添加任何元素。

  • 消費者使用super

如果需要一個列表使用T類型的元素(即你想把T類型的元素加入到列表中),你需要把這個列表聲明成《? super T》,比如List《? super Integer》,因此你不能保證從中讀取到的元素的類型。

  • 即是生產者,也是消費者

如果一個列表即要生產,又要消費,你不能使用泛型通配符聲明列表,比如List。

請參考java.util.Collections裏的copy方法(JDK1.7):copy使用到了PECS原則,實現了對參數的保護。

/**
     * 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());
            }
        }
    }
發佈了39 篇原創文章 · 獲贊 11 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章