前两篇文章介绍了泛型的基本用法、类型擦除以及泛型数组。在泛型的使用中,还有个重要的东西叫通配符,本文介绍通配符的使用。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
数组的引用。这是有意义的,Apple
是 Fruit
的子类,一个 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>();
上面的代码根本就没法编译。当涉及到泛型时, 尽管 Apple
是 Fruit
的子类型,可是 ArrayList<Apple>
不是 ArrayList<Fruit>
的子类型,泛型不支持协变。ui
从上面咱们知道,List<Number> list = ArrayList<Integer>
这样的语句是没法经过编译的,尽管 Integer
是 Number
的子类型。那么若是咱们确实须要创建这种 “向上转型” 的关系怎么办呢?这就须要通配符来发挥做用了。设计
利用 <? 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>
,可是为了向上转型给 flist
,flist
并不关心这个具体类型是什么。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
却能够调用 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 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
的类型是 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
自己 ) 的一种。大多数状况下泛型的使用比较简单,可是若是本身编写支持泛型的代码须要对泛型有深刻的了解。这几篇文章介绍了泛型的基本用法、类型擦除、泛型数组以及通配符的使用,涵盖了最经常使用的要点,泛型的总结就写到这里。
参考
若是个人文章对您有帮助,不妨点个赞支持一下(^_^)