Java泛型 - 通配符以及原始類型(Raw Type)

參考 & 推薦

推薦閱讀:


如何理解List<? extends Number>

根據定義, List<? extends Number> list指的是list引用可以指向聲明為List<繼承于Number>的實例(不一定要是直接父類, 祖先有Number即可).
比如說以下都是合法的,

List<? extends Number> listOfNumbers = new ArrayList<Number>();
List<? extends Number> listOfIntegers = new ArrayList<Integer>();
List<? extends Number> listOfDoubles = new ArrayList<Double>();

但是我們卻不能向上面任何一個容器加入數據.

List<? extends Number> listOfIntegers = new ArrayList<Integer>();
listOfIntegers.add(100);  // 錯誤, 不允許添加

用呆杰的話來理解就是,
現在給了你一個List<? extends Number>的引用listOfNumber, 他可能是任何繼承于Number的List<>. 如果允許往其中加入數據的話很顯然是不安全的, 比如說調用list.add(1.4), 但是list實際上指向的是List<Integer>類型, 這樣很顯然是不允許的.
List<? extends Number>用處是什么? 一個常見的用例就是作為函數參數類型, 因為雖然我們不能對List<? extends Number>的引用進行寫操作, 但是我們可以讀內容. 因為能傳進來的List的泛型類型都是繼承于Number類的, 所以總是能將其元素安全地轉換為Number類. 比如下面這個例子:

private static double sumList(List<? extends Number> list) {
    return list.stream()
            .mapToDouble(Number::doubleValue) // returns DoubleStream
            .sum();
}

public static void main(String[] args) {
    List<Integer> ints = Arrays.asList(1, 2, 3, 4, 5);
    List<Double> doubles = Arrays.asList(1.0, 2.0, 3.0, 4.0, 5.0);
    List<BigDecimal> bigDecimals = Arrays.asList(
        new BigDecimal("1.0"),
        new BigDecimal("2.0"),
        new BigDecimal("3.0"),
        new BigDecimal("4.0"),
        new BigDecimal("5.0")
    );

    System.out.printf("ints sum is        %s%n", sumList(ints));
    System.out.printf("doubles sum is     %s%n", sumList(doubles));
    System.out.printf("bigdecimals sum is %s%n", sumList(bigDecimals));
}

可能我們會有疑問, 為什么不直接把參數類型定義為List<Number>呢?為什么非得加上? extends看起來如此復雜的聲明? 答案是, List<Number>List<Integer>其實沒有任何關系, 并沒有List<Integer>List<Number>的子類的意思. 其實, 因為泛型擦除的原因, 這兩個類最終都是List. 從下面的例子可以看出List<People>List<Man>并沒有什么關系. 所以Java中的泛型是不協變的, 即A是B的父類, 但是List<A>和List<B>并沒有關系.
更多協變內容: Treant - Java中的逆變與協變
下面代碼表明了Java的泛型不是協變的.

    class People{

    }

    class Man extends People{

    }

    class Boy extends Man{

    }

    public void test(){
        List<People> peopleList = new ArrayList<People>();
        List<Man> manList = new ArrayList<Man>();
        peopleList = (List<People>) manList; // 錯誤, 不能轉換類型
    }

原文中給出的例子:

List<String> strings = new ArrayList<>();
String s = "abc";
Object o = s;      // allowed
// strings.add(o); // not allowed

// List<Object> moreObjects = strings; // also not allowed, but pretend it was
// moreObjects.add(new Date());
// String s = moreObjects.get(0); // uh oh
// 感覺按照下面的解釋, 這里應該是 String s = strings.get(0);

Since String is a subclass of Object, you can assign a String reference to an Object reference. You can’t however, add an Object reference to a List<String>, which feels strange. The problem is that List<String> is NOT a subclass of List<Object>. When declaring a type, the only instances you can add to it are of the declared type. That’s it. No sub- or superclass instances allowed. We say that the parameterized type is invariant.

The commented out section shows why List<String> is not a subclass of List<Object>. Say you could assign a list of strings to a reference to a list of objects. Then, using the list of objects reference, you could add something that wasn’t a string to the list, which would cause a cast exception when you tried to retrieve it using the original reference to the list of strings. The compiler wouldn’t know any better.

這段話主要說明了List<TypeA>List<TypeB>是沒有什么關系的. 如果List<String>能轉型為List<Object>那么我們就可以往List<String>里面加入其他類型的對象, 這顯然是不正確的, 所以泛型類并不是協變的.


如何理解 <? super>

List<? super Number> list 表明list引用可以指向元素類型為Number或者Number的超類的List, 比如說List<Number>List<Object>.
實例:

public void numsUpTo(Integer num, List<? super Integer> output) {
    IntStream.rangeClosed(1, num)
        .forEach(output::add);
}

ArrayList<Integer> integerList = new ArrayList<>();
ArrayList<Number> numberList = new ArrayList<>();
ArrayList<Object> objectList = new ArrayList<>();

numsUpTo(5, integerList);
numsUpTo(5, numberList);
numsUpTo(5, objectList);

因為<? super Integer>所以, 往容器加Integer是絕對安全的, 因為實際的List要么是Integer要么是Integer的父類, 所以Integer引用一定能轉型為Integer或者Integer的父類引用.

實例2(Collections類的max方法):

public static <T> T max(Collection<? extends T> coll, Comparator<? super T> comp) {
        if (comp==null)
            return (T)max((Collection) coll);

        Iterator<? extends T> i = coll.iterator();
        T candidate = i.next();

        while (i.hasNext()) {
            T next = i.next();
            if (comp.compare(next, candidate) > 0)
                candidate = next;
        }
        return candidate;
    }

注意Comparator<? super T> comp部分, 在Comparator的泛型參數中使用了super, 表明可以使用T的父類的比較方法.
注意對比以下幾個方法聲明:

public static <T> T max(Collection<? extends T> collection, Comparator<T> comparator){
        return null;
}

public static <T> T max2(Collection<T> collection, Comparator<T> comparator){
        return null;
}

然后有Father和Son類:

static class Father{

}

static class Son extends Father{

}

測試:

public static void main(String[] args) {
    List<Son> sons = new ArrayList<Son>();
    Collections.max(sons, new Comparator<Father>(){
        @Override
        public int compare(Father o1, Father o2) {
            return 0;
        }
    });

    max(sons, new Comparator<Father>(){
        @Override
        public int compare(Father o1, Father o2) {
            return 0;
        }
    });

//    不能這樣調用max2
//    max2(sons, new Comparator<Father>(){
//        @Override
//        public int compare(Father o1, Father o2) {
//            return 0;
//        }
//    });
}

其中max2的調用是錯誤的, 因為max2的參數表明該容器存放的類型必須實現了跟自己比較的Comparator.
但是為什么max(Collection<? extends T> collection, Comparator<T> comparator)Comparator的參數沒用super但是例子中調用卻是合法的呢? 這是因為對于調用max(sons, new Comparator<Father>(){...}), Java推斷了類型參數<T>Father, 而Collection<? extends Father>表明是可以傳入存放Son類型的容器的.


如何合理使用通配符

PECS - Producer - Extends, Consumer - Super, 這個詞來來源Effective Java一書.

  • Producer
    這里生產者的意思是, 你要從某個參數中獲取某個類型的數據, 那么聲明這個參數類型為<? extends T>. 比如說List<? extends Number> list, 表明list是一個生產者, 你可以從list中取出Number對象.
  • Consumer
    這里消費者的意思是, 這個參數將消費(使用)到某個類型的數據, 那么應該將參數聲明為<? super T>. 比如說Collection<? super E> coll, 表明coll可以消費E類型的數據.
  • 即要消費又要生產
    那么就不使用通配符.

java 官方文檔也有關于使用通配符的建議.

下面是一些例子, 多數來自Effective Java (2nd Edition).

實例

static <E> E reduce(List<E> list, Function<E> f, E iniVal);  // #1

list僅僅用于produce類型為E的數據, 所以符合producer的角色, 所以應該將其聲明為List<? extends E>. 而Function<E> f既要消費E又會產生E, 所以直接使用具體類型. 修改后的聲明如下:

static <E> E reduce(List<? extends E> list, Function<E> f, E iniVal); // #2

那么上面兩者有什么區別呢? 對于#1, 當Function<E> fFunction<Number>的時候, 對于List<Integer> listOfIntegers來說, 是傳不進去的, 只能是List<Number>. 但是#2, 因為listList<? extends E>, 所以此處可以傳入listOfIntegers.

  1. 類型推導
public static <E> Set<E>  union(Set<E> s1, Set<E> s2);

s1和s2都是producer, 所以修改為以下聲明

public static <E> Set<E>  union(Set<? extends E> s1, Set<? extends E> s2);

注意返回值仍然是Set<E>, 而不是Set<? extedns E>. 如果改成后者, 那么用戶代碼也必須使用通配符, 這是一個不好的決定.
類型推導的規則十分復雜, 在[JLS, 15.12.2.7-8]中有整整16頁描述. 雖然大多數情況下, 用戶無需指定類型參數, 但是對于有些情況, 則必須由用戶指定邊界的類型到底是什么.

Set<Integer> integers = ...;
Set<Double> doubles = ...;
Set<Number> numbers = union(integers, doubles);

感覺上Java應該推斷E為Number, 但是在不指定具體類型參數的時候, 卻會報錯.

注意: 在我個人實驗這段代碼的時候, 編譯器已經正確推導出了類型.(Java 1.8), 因為Effective Java (2nd Edition)出版于2008年, 所以應該是老版本編譯器的問題.

顯式指定類型參數:
注意以下幾點:

  • class后面的類型參數是無法被靜態方法使用的, 靜態方法必須自己重新定義類型參數
    這個和class的類型參數不能用于靜態方法一樣, 因為靜態屬性和方法都是整個類共有的, 如果有其他地方傳入了兩種不同的類型, 那么靜態屬性或者方法不可能同時擁有兩種類型, 所以這是不被允許的. 可見我的另一篇文章Java 泛型使用限制.
  • 顯式指定類型參數的靜態泛型方法的調用格式:
    ClassName.<Type...>methodName();
    <>后和方法名之間不用再加.
public class TypeInference {

    public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2){
        Set<E> result = new HashSet<>();
        result.addAll(s1);
        result.addAll(s2);
        return result;
    }

    public static void main(String[] args) {
        Set<Integer> integerSet = new HashSet<>();
        Set<Double> doubleSet = new HashSet<>();
        Set<Number> numberSet = union(integerSet, doubleSet);
        // 顯式指定
        numberSet = TypeInference.<Number>union(integerSet, doubleSet);
    }
}
  1. max方法的聲明
public static <T extends Comparable<T>> T max(List<T> list);  // #1

修改過后

public static <T extends Comparable<? super T>> T max(List<? extends T> list);  // #2

那么#2的優點在哪呢? 首先對于Comparable<? super T>表明, 可以用其父類的比較函數來比較子類. 對于list參數, 是對PECS的應用, 但是我個人認為list在這個語境下即使被定義為List<T> list也能有同樣的效果.

  1. List<?> 和 List<E>
public static <E> void swap(List<E> list,   int i,  int j); // #1
public static void swap(List<?> list,   int i,  int j);  // #2

對于#1, list能夠get也能add, 對于#2只能取出Object, 而且只有null能作為add的參數.
基于#2的交換代碼:

public static void swap(List<?> list,   int i,  int j){
  list.set(i, list.set(j, list.get(i)));
}

我們會發現, 編譯器不會通過這段代碼, 看起來很違背直覺. 從同一個列表拿出的元素竟然不能放回去. 這是因為<?>, 編譯知道list中的元素是某個具體類型, 但是因為是?, 所以并不知道具體是哪個類型. 所以從listget方法中拿出的數據, 只能是Object引用, 這樣才安全. 對于set()方法, 除了null, 編譯器不會允許我們放入任何其他東西, 因為編譯器無法判斷我們要添加的東西到底是不是?的那個類型, 所以就會阻止我們這么做.
而使用#1的聲明, 這一操作就可以執行了, 因為編譯器知道list中的元素可以安全的轉換為E, 而add方法由于現在有了E, 也知道, 可以安全的放入E對象, 所以swap就可以正常工作.

書上提到<?>會比<E>看起來是更好的API聲明, 像swap函數, 對外的聲明仍然是#2形式, 然后內部實現采用#1的私有函數來做. 這里也涉及到一個概念叫capture, 有些編譯錯誤中會有capture, 實際是指的是編譯器為不確定的?類型定義了一個名字而已. 詳細可見capture.


<?> 與 Raw Type

stackoverflow上有一篇討論raw type的提問, 里面講到了Raw Type和<?>的區別.
what-is-a-raw-type-and-why-shouldnt-we-use-it
使用了<?>的話, compiler會進行類型檢查, 所以不能夠通過一個List<?>的引用, 往List實例中添加任何元素(null除外, 因為null可以賦值給任何引用對象), 因為根本無法確定List<?>到底指向了什么類型的List, 所以無法保證類型安全, 所以不能通過這種引用添加元素.

static void appendNewObject(List<?> list) {
    list.add(new Object()); // compilation error!
}

但是, 如果參數是List這種Raw Type, 那么添加任何元素都是可以的:

List list = new ArrayList<Integer>();
list.add(0);
list.add("what");

上面這段代碼是可以運行的, 但是compiler會給出警告.
在引入泛型以后, 使用Raw Type是不被推薦的, 使用Raw Type只是為了兼容性問題!
例外情況, 因為Java泛型擦除的關系, List<String>.class是錯誤的, 因為Java泛型沒有生成新的class, 所以當需要引用List這個class的時候, 必須使用List.class, 同理使用instanceof操作符的時候, 也只能用o instanceof Set而不能夠o instanceof Set<String>.

List, List<?>, List<Object> 區別

    public static void testFunction(List<Integer> integerList){
        // do nothing
    }

    public static void main(String[] args) {
        List rawList = new ArrayList();
        List<?> wildcardList = new ArrayList<>();
        List<Object> objectList = new ArrayList<>();

        // 編譯器不會阻止我們添加任何對象, 但是會給出一個警告
        rawList.add("we can add anything to rawList");

        // 編譯器直接拒絕這個操作
        // wildcardList.add("we cannot add anything, because the compiler don't know what the exact type for ? is");

        // 可以添加任何對象
        objectList.add("we can add anything too, because everything is derived from Object");

        // 同樣是warning, 但是允許傳入, 這很不安全, 但是raw type就是可以這樣做
        // 所以raw type可作為任何List<AnyType>的參數
        testFunction(rawList);

        // 編譯器不允許
        // testFunction(wildcardList);

        // 也是不允許的
        // testFunction(objectList);

        // 所以使用RawType確實是很危險的, 因為編譯器只會給警告, 而不會阻止我們做一些潛在危險的事情
    }
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容