中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

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

發布時間:2020-10-05 17:56:03 來源:腳本之家 閱讀:162 作者:然則 欄目:編程語言

簡介

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

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

  • Java 泛型總結(一):基本用法與類型擦除
  • Java 泛型總結(二):泛型與數組

數組的協變

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

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 數組的引用。這是有意義的,Apple 是 Fruit 的子類,一個 Apple 對象也是一種 Fruit 對象,所以一個 Apple 數組也是一種 Fruit 的數組。這稱作數組的協變,Java 把數組設計為協變的,對此是有爭議的,有人認為這是一種缺陷。

盡管 Apple[] 可以 “向上轉型” 為 Fruit[],但數組元素的實際類型還是 Apple,我們只能向數組中放入 Apple或者 Apple 的子類。在上面的代碼中,向數組中放入了 Fruit 對象和 Orange 對象。對于編譯器來說,這是可以通過編譯的,但是在運行時期,JVM 能夠知道數組的實際類型是 Apple[],所以當其它對象加入數組的時候就會拋出異常。

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

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

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

使用通配符

從上面我們知道,List<Number> list = ArrayList<Integer> 這樣的語句是無法通過編譯的,盡管 Integer 是 Number 的子類型。那么如果我們確實需要建立這種 “向上轉型” 的關系怎么辦呢?這就需要通配符來發揮作用了。

上邊界限定通配符

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

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> 但是為了向上轉型給 flist,flist 并不關心這個具體類型是什么。

如上所述,通配符 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 卻可以調用 contains 和 indexOf 方法,它們都接受了一個 Apple 對象做參數。如果查看 ArrayList 的源代碼,可以發現 add() 接受一個泛型類型作為參數,但是 contains 和 indexOf 接受一個 Object 類型的參數,下面是它們的方法簽名:

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

所以如果我們指定泛型參數為 <? extends Fruit> 時,add() 方法的參數變為 ? extends Fruit,編譯器無法判斷這個參數接受的到底是 Fruit 的哪種類型,所以它不會接受任何類型。

然而,contains 和 indexOf 的類型是 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>,取出的元素轉型為 T。dest 是要寫入的目標 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 本身 ) 的一種。

大多數情況下泛型的使用比較簡單,但是如果自己編寫支持泛型的代碼需要對泛型有深入的了解。這幾篇文章介紹了泛型的基本用法、類型擦除、泛型數組以及通配符的使用,涵蓋了最常用的要點,泛型的總結就寫到這里。

以上就是本文的全部內容,希望本文的內容對大家的學習或者工作能帶來一定的幫助,同時也希望多多支持億速云!

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

泰来县| 电白县| 绥阳县| 郴州市| 沅江市| 应城市| 晋中市| 玉门市| 西峡县| 勐海县| 济阳县| 南宫市| 惠来县| 西畴县| 大港区| 广丰县| 平凉市| 新源县| 岳普湖县| 永顺县| 阿坝县| 白银市| 浦县| 闸北区| 靖州| 谢通门县| 巧家县| 桂东县| 武安市| 镇康县| 鄂伦春自治旗| 阿瓦提县| 佛山市| 体育| 德钦县| 霞浦县| 凤城市| 阳东县| 镇原县| 阿拉善右旗| 全州县|