Java 泛型總結(三):通配符的使用

簡介

前兩篇文章介紹了泛型的基本用法、類型擦除以及泛型數組。在泛型的使用中,還有個重要的東西叫通配符,本文介紹通配符的使用。java

這個系列的另外兩篇文章:編程

數組的協變

在瞭解通配符以前,先來了解一下數組。Java 中的數組是協變的,什麼意思?看下面的例子:segmentfault

class Fruit {}
class Apple extends Fruit {}
class Jonathan extends Apple {}
class Orange extends Fruit {}

public class CovariantArrays {
    public static void main(String[] args) {       
        Fruit[] fruit = new Apple[10];
        fruit[0] = new Apple(); // OK
        fruit[1] = new Jonathan(); // OK
        // Runtime type is Apple[], not Fruit[] or Orange[]:
        try {
            // Compiler allows you to add Fruit:
            fruit[0] = new Fruit(); // ArrayStoreException
        } catch(Exception e) { System.out.println(e); }
        try {
            // Compiler allows you to add Oranges:
            fruit[0] = new Orange(); // ArrayStoreException
        } catch(Exception e) { System.out.println(e); }
        }
} /* Output:
java.lang.ArrayStoreException: Fruit
java.lang.ArrayStoreException: Orange
*///:~

main 方法中的第一行,建立了一個 Apple 數組並把它賦給 Fruit 數組的引用。這是有意義的,AppleFruit 的子類,一個 Apple 對象也是一種 Fruit 對象,因此一個 Apple 數組也是一種 Fruit 的數組。這稱做數組的協變,Java 把數組設計爲協變的,對此是有爭議的,有人認爲這是一種缺陷。數組

儘管 Apple[] 能夠 「向上轉型」 爲 Fruit[],但數組元素的實際類型仍是 Apple,咱們只能向數組中放入 Apple或者 Apple 的子類。在上面的代碼中,向數組中放入了 Fruit 對象和 Orange 對象。對於編譯器來講,這是能夠經過編譯的,可是在運行時期,JVM 可以知道數組的實際類型是 Apple[],因此當其它對象加入數組的時候就會拋出異常。安全

泛型設計的目的之一是要使這種運行時期的錯誤在編譯期就能發現,看看用泛型容器類來代替數組會發生什麼:app

// Compile Error: incompatible types:
ArrayList<Fruit> flist = new ArrayList<Apple>();

上面的代碼根本就沒法編譯。當涉及到泛型時, 儘管 AppleFruit 的子類型,可是 ArrayList<Apple> 不是 ArrayList<Fruit> 的子類型,泛型不支持協變。ui

使用通配符

從上面咱們知道,List<Number> list = ArrayList<Integer> 這樣的語句是沒法經過編譯的,儘管 IntegerNumber 的子類型。那麼若是咱們確實須要創建這種 「向上轉型」 的關係怎麼辦呢?這就須要通配符來發揮做用了。設計

上邊界限定通配符

利用 <? extends Fruit> 形式的通配符,能夠實現泛型的向上轉型:rest

public class GenericsAndCovariance {
    public static void main(String[] args) {
        // Wildcards allow covariance:
        List<? extends Fruit> flist = new ArrayList<Apple>();
        // Compile Error: can’t add any type of object:
        // flist.add(new Apple());
        // flist.add(new Fruit());
        // flist.add(new Object());
        flist.add(null); // Legal but uninteresting
        // We know that it returns at least Fruit:
        Fruit f = flist.get(0);
    }
}

上面的例子中, flist 的類型是 List<? extends Fruit>,咱們能夠把它讀做:一個類型的 List, 這個類型能夠是繼承了 Fruit 的某種類型。注意,這並非說這個 List 能夠持有 Fruit 的任意類型。通配符表明了一種特定的類型,它表示 「某種特定的類型,可是 flist 沒有指定」。這樣不太好理解,具體針對這個例子解釋就是,flist 引用能夠指向某個類型的 List,只要這個類型繼承自 Fruit,能夠是 Fruit 或者 Apple,好比例子中的 new ArrayList<Apple>,可是爲了向上轉型給 flistflist 並不關心這個具體類型是什麼。code

如上所述,通配符 List<? extends Fruit> 表示某種特定類型 ( Fruit 或者其子類 ) 的 List,可是並不關心這個實際的類型究竟是什麼,反正是 Fruit 的子類型,Fruit 是它的上邊界。那麼對這樣的一個 List 咱們能作什麼呢?其實若是咱們不知道這個 List 到底持有什麼類型,怎麼可能安全的添加一個對象呢?在上面的代碼中,向 flist 中添加任何對象,不管是 Apple 仍是 Orange 甚至是 Fruit 對象,編譯器都不容許,惟一能夠添加的是 null。因此若是作了泛型的向上轉型 (List<? extends Fruit> flist = new ArrayList<Apple>()),那麼咱們也就失去了向這個 List 添加任何對象的能力,即便是 Object 也不行。

另外一方面,若是調用某個返回 Fruit 的方法,這是安全的。由於咱們知道,在這個 List 中,無論它實際的類型究竟是什麼,但確定能轉型爲 Fruit,因此編譯器容許返回 Fruit

瞭解了通配符的做用和限制後,好像任何接受參數的方法咱們都不能調用了。其實倒也不是,看下面的例子:

public class CompilerIntelligence {
    public static void main(String[] args) {
        List<? extends Fruit> flist =
        Arrays.asList(new Apple());
        Apple a = (Apple)flist.get(0); // No warning
        flist.contains(new Apple()); // Argument is ‘Object’
        flist.indexOf(new Apple()); // Argument is ‘Object’
        
        //flist.add(new Apple());   沒法編譯

    }
}

在上面的例子中,flist 的類型是 List<? extends Fruit>,泛型參數使用了受限制的通配符,因此咱們失去了向其中加入任何類型對象的例子,最後一行代碼沒法編譯。

可是 flist 卻能夠調用 containsindexOf 方法,它們都接受了一個 Apple 對象作參數。若是查看 ArrayList 的源代碼,能夠發現 add() 接受一個泛型類型做爲參數,可是 containsindexOf 接受一個 Object 類型的參數,下面是它們的方法簽名:

public boolean add(E e)
public boolean contains(Object o)
public int indexOf(Object o)

因此若是咱們指定泛型參數爲 <? extends Fruit> 時,add() 方法的參數變爲 ? extends Fruit,編譯器沒法判斷這個參數接受的究竟是 Fruit 的哪一種類型,因此它不會接受任何類型。

然而,containsindexOf 的類型是 Object,並無涉及到通配符,因此編譯器容許調用這兩個方法。這意味着一切取決於泛型類的編寫者來決定那些調用是 「安全」 的,而且用 Object 做爲這些安全方法的參數。若是某些方法不容許類型參數是通配符時的調用,這些方法的參數應該用類型參數,好比 add(E e)

當咱們本身編寫泛型類時,上面介紹的就有用了。下面編寫一個 Holder 類:

public class Holder<T> {
    private T value;
    public Holder() {}
    public Holder(T val) { value = val; }
    public void set(T val) { value = val; }
    public T get() { return value; }
    public boolean equals(Object obj) {
    return value.equals(obj);
    }
    public static void main(String[] args) {
        Holder<Apple> Apple = new Holder<Apple>(new Apple());
        Apple d = Apple.get();
        Apple.set(d);
        // Holder<Fruit> Fruit = Apple; // Cannot upcast
        Holder<? extends Fruit> fruit = Apple; // OK
        Fruit p = fruit.get();
        d = (Apple)fruit.get(); // Returns ‘Object’
        try {
            Orange c = (Orange)fruit.get(); // No warning
        } catch(Exception e) { System.out.println(e); }
        // fruit.set(new Apple()); // Cannot call set()
        // fruit.set(new Fruit()); // Cannot call set()
        System.out.println(fruit.equals(d)); // OK
    }
} /* Output: (Sample)
java.lang.ClassCastException: Apple cannot be cast to Orange
true
*///:~

Holer 類中,set() 方法接受類型參數 T 的對象做爲參數,get() 返回一個 T 類型,而 equals() 接受一個 Object 做爲參數。fruit 的類型是 Holder<? extends Fruit>,因此set()方法不會接受任何對象的添加,可是 equals() 能夠正常工做。

下邊界限定通配符

通配符的另外一個方向是 「超類型的通配符「: ? super TT 是類型參數的下界。使用這種形式的通配符,咱們就能夠 」傳遞對象」 了。仍是用例子解釋:

public class SuperTypeWildcards {
    static void writeTo(List<? super Apple> apples) {
        apples.add(new Apple());
        apples.add(new Jonathan());
        // apples.add(new Fruit()); // Error
    }
}

writeTo 方法的參數 apples 的類型是 List<? super Apple>,它表示某種類型的 List,這個類型是 Apple 的基類型。也就是說,咱們不知道實際類型是什麼,可是這個類型確定是 Apple 的父類型。所以,咱們能夠知道向這個 List 添加一個 Apple 或者其子類型的對象是安全的,這些對象均可以向上轉型爲 Apple。可是咱們不知道加入 Fruit 對象是否安全,由於那樣會使得這個 List 添加跟 Apple 無關的類型。

在瞭解了子類型邊界和超類型邊界以後,咱們就能夠知道如何向泛型類型中 「寫入」 ( 傳遞對象給方法參數) 以及如何從泛型類型中 「讀取」 ( 從方法中返回對象 )。下面是一個例子:

public class Collections { 
  public static <T> void copy(List<? super T> dest, List<? extends T> src) 
  {
      for (int i=0; i<src.size(); i++) 
        dest.set(i,src.get(i)); 
  } 
}

src 是原始數據的 List,由於要從這裏面讀取數據,因此用了上邊界限定通配符:<? extends T>,取出的元素轉型爲 Tdest 是要寫入的目標 List,因此用了下邊界限定通配符:<? super T>,能夠寫入的元素類型是 T 及其子類型。

無邊界通配符

還有一種通配符是無邊界通配符,它的使用形式是一個單獨的問號:List<?>,也就是沒有任何限定。不作任何限制,跟不用類型參數的 List 有什麼區別呢?

List<?> list 表示 list 是持有某種特定類型的 List,可是不知道具體是哪一種類型。那麼咱們能夠向其中添加對象嗎?固然不能夠,由於並不知道實際是哪一種類型,因此不能添加任何類型,這是不安全的。而單獨的 List list ,也就是沒有傳入泛型參數,表示這個 list 持有的元素的類型是 Object,所以能夠添加任何類型的對象,只不過編譯器會有警告信息。

總結

通配符的使用能夠對泛型參數作出某些限制,使代碼更安全,對於上邊界和下邊界限定的通配符總結以下:

  • 使用 List<? extends C> list 這種形式,表示 list 能夠引用一個 ArrayList ( 或者其它 List 的 子類 ) 的對象,這個對象包含的元素類型是 C 的子類型 ( 包含 C 自己)的一種。
  • 使用 List<? super C> list 這種形式,表示 list 能夠引用一個 ArrayList ( 或者其它 List 的 子類 ) 的對象,這個對象包含的元素就類型是 C 的超類型 ( 包含 C 自己 ) 的一種。

大多數狀況下泛型的使用比較簡單,可是若是本身編寫支持泛型的代碼須要對泛型有深刻的瞭解。這幾篇文章介紹了泛型的基本用法、類型擦除、泛型數組以及通配符的使用,涵蓋了最經常使用的要點,泛型的總結就寫到這裏。

參考

  • Java 編程思想

若是個人文章對您有幫助,不妨點個贊支持一下(^_^)

相關文章
相關標籤/搜索