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 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 本身 ) 的一种。
大多数情况下泛型的使用比较简单,但是如果自己编写支持泛型的代码需要对泛型有深入的了解。这几篇文章介绍了泛型的基本用法、类型擦除、泛型数组以及通配符的使用,涵盖了最常用的要点,泛型的总结就写到这里。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,同时也希望多多支持!
下一篇: 【模板】可持久化线段树