簡介
前兩篇文章介紹了泛型的基本用法、類型擦除以及泛型數(shù)組。在泛型的使用中,還有個重要的東西叫通配符,本文介紹通配符的使用。
這個系列的另外兩篇文章:
Java 泛型總結(jié)(一):基本用法與類型擦除
Java 泛型總結(jié)(二):泛型與數(shù)組
數(shù)組的協(xié)變在了解通配符之前,先來了解一下數(shù)組。Java 中的數(shù)組是協(xié)變的,什么意思?看下面的例子:
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 方法中的第一行,創(chuàng)建了一個 Apple 數(shù)組并把它賦給 Fruit 數(shù)組的引用。這是有意義的,Apple 是 Fruit 的子類,一個 Apple 對象也是一種 Fruit 對象,所以一個 Apple 數(shù)組也是一種 Fruit 的數(shù)組。這稱作數(shù)組的協(xié)變,Java 把數(shù)組設(shè)計為協(xié)變的,對此是有爭議的,有人認為這是一種缺陷。
盡管 Apple[] 可以 “向上轉(zhuǎn)型” 為 Fruit[],但數(shù)組元素的實際類型還是 Apple,我們只能向數(shù)組中放入 Apple或者 Apple 的子類。在上面的代碼中,向數(shù)組中放入了 Fruit 對象和 Orange 對象。對于編譯器來說,這是可以通過編譯的,但是在運行時期,JVM 能夠知道數(shù)組的實際類型是 Apple[],所以當(dāng)其它對象加入數(shù)組的時候就會拋出異常。
泛型設(shè)計的目的之一是要使這種運行時期的錯誤在編譯期就能發(fā)現(xiàn),看看用泛型容器類來代替數(shù)組會發(fā)生什么:
// Compile Error: incompatible types: ArrayListflist = new ArrayList ();
上面的代碼根本就無法編譯。當(dāng)涉及到泛型時, 盡管 Apple 是 Fruit 的子類型,但是 ArrayList
從上面我們知道,List
利用 extends Fruit> 形式的通配符,可以實現(xiàn)泛型的向上轉(zhuǎn)型:
public class GenericsAndCovariance { public static void main(String[] args) { // Wildcards allow covariance: List extends Fruit> flist = new ArrayList(); // 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
如上所述,通配符 List extends Fruit> 表示某種特定類型 ( Fruit 或者其子類 ) 的 List,但是并不關(guān)心這個實際的類型到底是什么,反正是 Fruit 的子類型,Fruit 是它的上邊界。那么對這樣的一個 List 我們能做什么呢?其實如果我們不知道這個 List 到底持有什么類型,怎么可能安全的添加一個對象呢?在上面的代碼中,向 flist 中添加任何對象,無論是 Apple 還是 Orange 甚至是 Fruit 對象,編譯器都不允許,唯一可以添加的是 null。所以如果做了泛型的向上轉(zhuǎn)型 (List extends Fruit> flist = new ArrayList
另一方面,如果調(diào)用某個返回 Fruit 的方法,這是安全的。因為我們知道,在這個 List 中,不管它實際的類型到底是什么,但肯定能轉(zhuǎn)型為 Fruit,所以編譯器允許返回 Fruit。
了解了通配符的作用和限制后,好像任何接受參數(shù)的方法我們都不能調(diào)用了。其實倒也不是,看下面的例子:
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>,泛型參數(shù)使用了受限制的通配符,所以我們失去了向其中加入任何類型對象的例子,最后一行代碼無法編譯。
但是 flist 卻可以調(diào)用 contains 和 indexOf 方法,它們都接受了一個 Apple 對象做參數(shù)。如果查看 ArrayList 的源代碼,可以發(fā)現(xiàn) add() 接受一個泛型類型作為參數(shù),但是 contains 和 indexOf 接受一個 Object 類型的參數(shù),下面是它們的方法簽名:
public boolean add(E e) public boolean contains(Object o) public int indexOf(Object o)
所以如果我們指定泛型參數(shù)為 extends Fruit> 時,add() 方法的參數(shù)變?yōu)?? extends Fruit,編譯器無法判斷這個參數(shù)接受的到底是 Fruit 的哪種類型,所以它不會接受任何類型。
然而,contains 和 indexOf 的類型是 Object,并沒有涉及到通配符,所以編譯器允許調(diào)用這兩個方法。這意味著一切取決于泛型類的編寫者來決定那些調(diào)用是 “安全” 的,并且用 Object 作為這些安全方法的參數(shù)。如果某些方法不允許類型參數(shù)是通配符時的調(diào)用,這些方法的參數(shù)應(yīng)該用類型參數(shù),比如 add(E e)。
當(dāng)我們自己編寫泛型類時,上面介紹的就有用了。下面編寫一個 Holder 類:
public class Holder{ 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 = new Holder (new Apple()); Apple d = Apple.get(); Apple.set(d); // Holder 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() 方法接受類型參數(shù) T 的對象作為參數(shù),get() 返回一個 T 類型,而 equals() 接受一個 Object 作為參數(shù)。fruit 的類型是 Holder extends Fruit>,所以set()方法不會接受任何對象的添加,但是 equals() 可以正常工作。
下邊界限定通配符通配符的另一個方向是 “超類型的通配符“: ? super T,T 是類型參數(shù)的下界。使用這種形式的通配符,我們就可以 ”傳遞對象” 了。還是用例子解釋:
public class SuperTypeWildcards { static void writeTo(List super Apple> apples) { apples.add(new Apple()); apples.add(new Jonathan()); // apples.add(new Fruit()); // Error } }
writeTo 方法的參數(shù) apples 的類型是 List super Apple>,它表示某種類型的 List,這個類型是 Apple 的基類型。也就是說,我們不知道實際類型是什么,但是這個類型肯定是 Apple 的父類型。因此,我們可以知道向這個 List 添加一個 Apple 或者其子類型的對象是安全的,這些對象都可以向上轉(zhuǎn)型為 Apple。但是我們不知道加入 Fruit 對象是否安全,因為那樣會使得這個 List 添加跟 Apple 無關(guān)的類型。
在了解了子類型邊界和超類型邊界之后,我們就可以知道如何向泛型類型中 “寫入” ( 傳遞對象給方法參數(shù)) 以及如何從泛型類型中 “讀取” ( 從方法中返回對象 )。下面是一個例子:
public class Collections { public staticvoid copy(List super T> dest, List extends T> src) { for (int i=0; i src 是原始數(shù)據(jù)的 List,因為要從這里面讀取數(shù)據(jù),所以用了上邊界限定通配符: extends T>,取出的元素轉(zhuǎn)型為 T。dest 是要寫入的目標(biāo) List,所以用了下邊界限定通配符: super T>,可以寫入的元素類型是 T 及其子類型。
無邊界通配符還有一種通配符是無邊界通配符,它的使用形式是一個多帶帶的問號:List>,也就是沒有任何限定。不做任何限制,跟不用類型參數(shù)的 List 有什么區(qū)別呢?
List> list 表示 list 是持有某種特定類型的 List,但是不知道具體是哪種類型。那么我們可以向其中添加對象嗎?當(dāng)然不可以,因為并不知道實際是哪種類型,所以不能添加任何類型,這是不安全的。而多帶帶的 List list ,也就是沒有傳入泛型參數(shù),表示這個 list 持有的元素的類型是 Object,因此可以添加任何類型的對象,只不過編譯器會有警告信息。
總結(jié)通配符的使用可以對泛型參數(shù)做出某些限制,使代碼更安全,對于上邊界和下邊界限定的通配符總結(jié)如下:
使用 List extends C> list 這種形式,表示 list 可以引用一個 ArrayList ( 或者其它 List 的 子類 ) 的對象,這個對象包含的元素類型是 C 的子類型 ( 包含 C 本身)的一種。
使用 List super C> list 這種形式,表示 list 可以引用一個 ArrayList ( 或者其它 List 的 子類 ) 的對象,這個對象包含的元素就類型是 C 的超類型 ( 包含 C 本身 ) 的一種。
大多數(shù)情況下泛型的使用比較簡單,但是如果自己編寫支持泛型的代碼需要對泛型有深入的了解。這幾篇文章介紹了泛型的基本用法、類型擦除、泛型數(shù)組以及通配符的使用,涵蓋了最常用的要點,泛型的總結(jié)就寫到這里。
參考
Java 編程思想
如果我的文章對您有幫助,不妨點個贊支持一下(^_^)
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/65989.html
摘要:總結(jié)數(shù)組與泛型的關(guān)系還是有點復(fù)雜的,中不允許直接創(chuàng)建泛型數(shù)組。本文分析了其中原因并且總結(jié)了一些創(chuàng)建泛型數(shù)組的方式。 簡介 上一篇文章介紹了泛型的基本用法以及類型擦除的問題,現(xiàn)在來看看泛型和數(shù)組的關(guān)系。數(shù)組相比于Java 類庫中的容器類是比較特殊的,主要體現(xiàn)在三個方面: 數(shù)組創(chuàng)建后大小便固定,但效率更高 數(shù)組能追蹤它內(nèi)部保存的元素的具體類型,插入的元素類型會在編譯期得到檢查 數(shù)組可以持...
摘要:在這種情況下,編譯器認為調(diào)用泛型方法后,其返回值被賦給一個類型的變量顯示地指明類型參考資料編程的邏輯 一、泛型的概念 泛型實現(xiàn)了參數(shù)化類型的概念,使代碼可以用于多種類型 二、泛型的目的 希望類和方法能夠具備最廣泛的表達能力 用來指定容器要持有什么類型的對象,而且由編譯器來保證類型的正確性 三、泛型的使用 普通泛型類 public class NormalGenericsClas...
摘要:然而中的泛型使用了類型擦除,所以只是偽泛型。總結(jié)本文介紹了泛型的使用,以及類型擦除相關(guān)的問題。一般情況下泛型的使用比較簡單,但是某些情況下,尤其是自己編寫使用泛型的類或者方法時要注意類型擦除的問題。 簡介 Java 在 1.5 引入了泛型機制,泛型本質(zhì)是參數(shù)化類型,也就是說變量的類型是一個參數(shù),在使用時再指定為具體類型。泛型可以用于類、接口、方法,通過使用泛型可以使代碼更簡單、安全。然...
摘要:靜態(tài)變量是被泛型類的所有實例所共享的。對于這個類型系統(tǒng),有如下的一些規(guī)則相同類型參數(shù)的泛型類的關(guān)系取決于泛型類自身的繼承體系結(jié)構(gòu)。在代碼中避免泛型類和原始類型的混用。參考泛型類型擦除 Java泛型總結(jié) Java泛型是JDK5引入的一個新特性,允許在定義類和接口的時候使用類型參數(shù)(type parameter)。聲明的類型參數(shù)在使用的時候使用具體的類型來替換。泛型最主要的應(yīng)用是在JDK5...
摘要:知識點總結(jié)泛型知識點總結(jié)泛型泛型泛型就是參數(shù)化類型適用于多種數(shù)據(jù)類型執(zhí)行相同的代碼泛型中的類型在使用時指定泛型歸根到底就是模版優(yōu)點使用泛型時,在實際使用之前類型就已經(jīng)確定了,不需要強制類型轉(zhuǎn)換。 Java知識點總結(jié)(Java泛型) @(Java知識點總結(jié))[Java, Java泛型] [toc] 泛型 泛型就是參數(shù)化類型 適用于多種數(shù)據(jù)類型執(zhí)行相同的代碼 泛型中的類型在使用時指定 泛...
閱讀 955·2021-11-17 09:33
閱讀 415·2019-08-30 11:16
閱讀 2468·2019-08-29 16:05
閱讀 3351·2019-08-29 15:28
閱讀 1393·2019-08-29 11:29
閱讀 1947·2019-08-26 13:51
閱讀 3385·2019-08-26 11:55
閱讀 1203·2019-08-26 11:31