在泛型的使用中,還有個重要的東西叫做通配符,本文介紹通配符的使用。具有很好的參考價值。下面跟著小編一起來看下吧
簡介
前兩篇文章介紹了泛型的基本用法、類型擦除以及泛型陣列。在泛型的使用中,還有一個重要的東西叫通配符,本文介紹通配符的使用。
陣列的協變
在了解通配符之前,先來了解陣列。 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 的子類型,但是 ArrayList463277d9ebc274bcf30ecc27cb72790a 不是 ArrayListe4dae6b035208b28264d9169d0b1fee3 的子類型,泛型不支援協變。
使用通配符
從上面我們知道,Listc8f01a3f8889dcf657849dd45bc0fc4c list = ArrayListc0f559cc8d56b43654fcbe4aa9df7b4a
這樣的語句是無法通過編譯的,儘管Integer 是Number 的子類型。那麼如果我們確實需要建立這種 「向轉型」 的關係怎麼辦呢?這就需要通配符來發揮作用了。
上邊界限定通配符
利用57019040ccef885c8e3bd8f9deb31922 形式的通配符,可以實現泛型的向上轉型:
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 的型別是List57019040ccef885c8e3bd8f9deb31922
我們可以把它讀作:一個型別的List, 這個型別可以是繼承#了Fruit的某種類型。注意,這並不是說這個 List 可以持有 Fruit 的任意型別。通配符代表了特定的類型,它表示 「某種特定的類型,但是 flist 沒有指定」。這樣不太好理解,具體針對這個例子解釋就是,flist 引用可以指向某個類型的List,只要這個類型繼承自Fruit,可以是Fruit 或者Apple,比如例子中的new ArrayList463277d9ebc274bcf30ecc27cb72790a
但為了向上轉型給flist,flist 並不在乎這個特定類型是什麼。
如上所述,通配符List57019040ccef885c8e3bd8f9deb31922
表示某種特定類型( Fruit 或其子類別) 的List,但是並不關心這個實際的類型到底是什麼,反正是Fruit 的子類型,Fruit 是它的上邊界。那麼對這樣的一個 List 我們能做什麼呢?其實如果我們不知道這個 List 到底持有什麼類型,怎麼可能安全的新增一個物件呢?在上面的程式碼中,向 flist 中添加任何對象,無論是 Apple 還是 Orange 甚至是 Fruit 對象,編譯器都不允許,唯一可以添加的是 null。所以如果做了泛型的向上轉型(List57019040ccef885c8e3bd8f9deb31922 flist = new ArrayList463277d9ebc274bcf30ecc27cb72790a()
),那麼我們也就失去了向這個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 的型別是List57019040ccef885c8e3bd8f9deb31922
,泛型參數使用了受限的通配符,所以我們失去了在其中加入任何類型物件的例子,最後一行程式碼無法編譯。
但是 flist 却可以调用 contains 和 indexOf 方法,它们都接受了一个 Apple 对象做参数。如果查看 ArrayList 的源代码,可以发现 add() 接受一个泛型类型作为参数,但是 contains 和 indexOf 接受一个 Object 类型的参数,下面是它们的方法签名:
public boolean add(E e) public boolean contains(Object o) public int indexOf(Object o)
所以如果我们指定泛型参数为 57019040ccef885c8e3bd8f9deb31922
时,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 的类型是 Holder57019040ccef885c8e3bd8f9deb31922
,所以set()方法不会接受任何对象的添加,但是 equals() 可以正常工作。
下边界限定通配符
通配符的另一个方向是 “超类型的通配符“: ? super T
,T
是类型参数的下界。使用这种形式的通配符,我们就可以 ”传递对象” 了。还是用例子解释:
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 的类型是 List72b4226105aa1d07ec3b6e98f565c59e
它表示某种类型的 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,因为要从这里面读取数据,所以用了上边界限定通配符:d203bb1ae585225d4838a2b7e3d0503e,取出的元素转型为 T。dest 是要写入的目标 List,所以用了下边界限定通配符:117c5a0bdb71ea9a9d0c2b99b03abe3e,可以写入的元素类型是 T 及其子类型。
无边界通配符
还有一种通配符是无边界通配符,它的使用形式是一个单独的问号:List6b3d0130bba23ae47fe2b8e8cddf0195,也就是没有任何限定。不做任何限制,跟不用类型参数的 List 有什么区别呢?
List6b3d0130bba23ae47fe2b8e8cddf0195 list
表示 list 是持有某种特定类型的 List,但是不知道具体是哪种类型。那么我们可以向其中添加对象吗?当然不可以,因为并不知道实际是哪种类型,所以不能添加任何类型,这是不安全的。而单独的 List list
,也就是没有传入泛型参数,表示这个 list 持有的元素的类型是 Object
,因此可以添加任何类型的对象,只不过编译器会有警告信息。
总结
通配符的使用可以对泛型参数做出某些限制,使代码更安全,对于上边界和下边界限定的通配符总结如下:
使用 Listb75ffcf7a0002c20418af7f7df067052 list
这种形式,表示 list 可以引用一个 ArrayList
( 或者其它 List 的 子类 ) 的对象,这个对象包含的元素类型是 C 的子类型 ( 包含 C 本身)的一种。
使用 List41a5f90752881873ec48ff3be0e4f0a1 list
这种形式,表示 list 可以引用一个 ArrayList
( 或者其它 List 的 子类 ) 的对象,这个对象包含的元素就类型是 C 的超类型 ( 包含 C 本身 ) 的一种。
大多数情况下泛型的使用比较简单,但是如果自己编写支持泛型的代码需要对泛型有深入的了解。这几篇文章介绍了泛型的基本用法、类型擦除、泛型数组以及通配符的使用,涵盖了最常用的要点,泛型的总结就写到这里。
以上是Java泛型總結(三)-詳解通配符的使用的詳細內容。更多資訊請關注PHP中文網其他相關文章!