首頁 > Java > java教程 > 主體

Java泛型總結(三)-詳解通配符的使用

黄舟
發布: 2017-03-22 10:25:10
原創
1484 人瀏覽過

在泛型的使用中,還有個重要的東西叫做通配符,本文介紹通配符的使用。具有很好的參考價值。下面跟著小編一起來看下吧

簡介

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

陣列的協變

在了解通配符之前,先來了解陣列。 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 不是 ArrayList 的子類型,泛型不支援協變。

使用通配符

從上面我們知道,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&#39;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&#39;
 flist.indexOf(new Apple()); // Argument is ‘Object&#39;
 //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&#39;
 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,因为要从这里面读取数据,所以用了上边界限定通配符:,取出的元素转型为 T。dest 是要写入的目标 List,所以用了下边界限定通配符:,可以写入的元素类型是 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泛型總結(三)-詳解通配符的使用的詳細內容。更多資訊請關注PHP中文網其他相關文章!

相關標籤:
來源:php.cn
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板
關於我們 免責聲明 Sitemap
PHP中文網:公益線上PHP培訓,幫助PHP學習者快速成長!