什么是通配符
通配符类型中, 允许类型参数变化。例如, 通配符类型
Pair<? extends Employee〉
表示任何泛型Pair 类型, 它的类型参数是Employee 的子类。
为什么会存在这种通配符存在呢?比如有一个水果(Fruit)类和苹果(Apple)类,苹果继承于水果,现在有一个盘子,这个盘子按照我们人类的思维来看他可以装水果,那么也可以装苹果
public class Plate<T> {
private T item;
public T getItem() {
return item;
}
public void setItem(T item) {
this.item = item;
}
}
现在如下,我们想要打印这个盘子放了什么东东,我们想要复用这个打印的方法,就会发现不允许,因为我们已经限定了printPlate这个方法入参盘子应该是什么类型的,甚至连他的子类都不允许。
public class GenMain {
public static void main(String[] args) {
Plate<Fruit> fruitPlate=new Plate<>();
printPlate(fruitPlate);
Plate<Apple> applePlate=new Plate<>();
printPlate(applePlate);//error
}
public static void printPlate(Plate<Fruit> fruitPlate){
System.out.println(fruitPlate.getItem());
}
}
Java中泛型是不变的,可有时需要实现逆变与协变,怎么办呢?这时,通配符?
派上了用场。那什么是协变逆变呢?协变就是用一个窄类型替代宽类型,而逆变则用宽类型覆盖窄类型。
我们需要用到通配符Plate<? extends Fruit>
来解决我们的问题。
public class GenMain {
public static void main(String[] args) {
Plate<Fruit> fruitPlate=new Plate<>();
Plate<Apple> applePlate=new Plate<>();
printPlate(fruitPlate);
printPlate(applePlate);//right
}
public static void printPlate(Plate<? extends Fruit> fruitPlate){
System.out.println(fruitPlate.getItem());
}
}
通配符和泛型区别
我们学习了通配符和泛型可能就会觉得这两个好像啊,他们有什么区别?首先,类型参数<T>
是用于声明一个泛型类或者泛型方法,而<?>
是使用泛型类或者泛型方法,也就是说定义泛型之后,我们可能不确定我们会传什么具体的类型,我们可以用一个不确定的类型(也就是通配符)比如我们只知道我们要传递的是Fruit
的子类或者自身类型作为参数,那么我们就需要使用通配符。
几种通配符类型
上界通配符(Upper Bounds Wildcards)
例如:Plate<? extends Fruit>
,它实现了泛型的协变。也就是说这是一个能放水果以及一切是水果派生类(如Apple
)的盘子
如果把Fruit和Apple的例子再扩展一下,食物分成水果和肉类,水果有苹果和香蕉,肉类有猪肉和牛肉,苹果还有两种青苹果和红苹果。
上界通配符 Plate<? extends Fruit>
覆盖下图中蓝色的区域。
那我们把plate
换成List
,再看这个例子,
List<? extends Fruit> list=new ArrayList<Apple>( );
list.add(new Apple());//error
list.add(new Banana());//error
我们首先需要知道的是List
存放数据的时候只能存放一种类型的数据,所以我们指定了存放Apple
,那么为什么我们不能add苹果呢?我们知道Java的泛型是伪泛型,在1.5之前是没有泛型的,Java为了兼容以前的代码,所以泛型的类型会被擦除。所以使用泛型时,为了安全,所有与泛型相关的异常都应该在编译期间发现,因此为了泛型的绝对安全,java在设计时做了相关的限制:在编译之前先检查类型以保证泛型的运行安全,再执行编译。所以编译器是不知道list
具体是什么类型的,既然类型被擦除List
编译的时候会 是List<Object>
,那按道理Object
是所有的类的父类,为什么不能往里面add
?那我们思考一下泛型的出现是为了什么?就是为了解决类型转换的问题,在没有泛型之前,使用ArrayList
需要做强制转换,现在有了泛型很明显这不符合它的设计初衷,所以禁止也是正常。
那为什么我们可以get
呢?因为提取数据的时候他的子类很容易隐式转成他的父类类型,这是安全的操作,所以也就是可以get
下界通配符(Lower Bounds Wildcards)
例如:Plate<? super Fruit>
,它实现了泛型的逆变。也就是说这是一个能放水果以及一切是水果基类(如Food
)的盘子,以及水果父类.对应上面的例子,Plate<? super Fruit>覆盖下图中红色的区域。也就是我们只知道我们到时候会往盘子里装可能是Food
、Fruit
类型的东西。
同理super
我们可以往里面set
他的子类以及自身类型的数据,因为子类很容易隐性转成父类型,但是不能使用get
,因为我们只知道它存入的数据可能是Fruit
的任何一个父类型的元素,会涉及类型强制转换的问题,所以为了绝对安全就不允许get
上界下界的长短处
我们引用知乎的一个回答:
<? extend Fruit> 和 <? super Fruit> 是两种箱子。
第一种箱子,Apple <? extend Fruit> appleBox,这种水果箱子只能装苹果,已经装好一些苹果。现在来了一个人,从这个箱子里面拿(get)了一个苹果出来,可以确定的是这个苹果一定是水果,但如果这个人想往里面装(set)葡萄,是不允许的,因为葡萄虽然是水果,但这个箱子只能装苹果。所以,可读不可写。
第二种箱子,Food<? super Fruit> foodBox, 这种食物箱子可以装水果等食物,已经装好了一些食物。现在来了一个人,从这个箱子里面拿(get)了一件食物出来,我们无法确定这个食物一定是水果,但如果这个人想往里面(set)葡萄,是允许的,因为葡萄属于水果,当然也属于食物了。所以,可写不可读。
我觉得解释的十分明晰,也就是PECS原则 (Producer Extends Consumer Super)。
无界通配符
还可以使用无限定的通配符, 例如,Pair<?>
。初看起来,这好像与原始的Pair 类型一样。实际上, 有很大的不同。类型Pair<?> 有以下方法:
? getFirst()
void setFirst (?)
getFirst
的返回值只能赋给一个Object
。setFirst
方法不能被调用, 甚至不能用Object
调用。Pair<?>
和Pair
本质的不同在于: 可以用任意Object
对象调用原始Pair
类的setObject
方法。(他可以setFirst(null)
)
为什么要使用这样脆弱的类型? 它对于许多简单的操作非常有用。例如,下面这个方法
将用来测试一个pair 是否包含一个null引用,它不需要实际的类型。
public static boolean hasNulls(Pair<?> p)
{
return p.getFirst() == null || p.getSecond()==null;
}
通过将hasNulls
转换成泛型方法,可以避免使用通配符类型:public static <T> boolean hasNulls(Pair<T> p)
,但是,带有通配符的版本可读性更强。
List
与List<?>
是否有区别?
List
,由于类型擦除的关系,List
跟List<Object>
是等价的,因此List表示的是类型参数是Object的List。
List<?>
,表示我这里的类型参数其实是特定的,但是,只是一时没有确定下来。
举个简单的例子:
List<? > list=Lists.newArrayList();
list.add(new Apple());//error
编译报错信息
我们可以关注到,编译的结果并不是使用Object
来代替?
,而是使用了capture#1
来代替,所以你想往里面add显然是add不进去的。
新的疑问
那你到这里大概已经分清楚了? extends T
,? super T
这两个的用法,但是你可能会提出疑问,那么这两个有什么用处呢,一个能get
不能set
,一个能set
不能get
,那么这里就要走出一个误区了,上面的做法仅仅只是作为一个刻意的例子,真正的使用是如开头的例子一样,通配符能够约束传入的类型,以便于我们可以拿到他的一些属性方法,然后做成一个方法族,可以简化大量的重复代码,不然你可能想打印苹果就要再写一个接收入参是Plate<Apple>
的方法。
我们以guava
的Lists
类作为例子,这里
List<? extends Fruit> list=Lists.newArrayList(new Banana(),new Apple());
//guava的newArrayList方法
@SafeVarargs
@GwtCompatible(serializable = true)
public static <E> ArrayList<E> newArrayList(E... elements) {
checkNotNull(elements); // for GWT
// Avoid integer overflow when a large array is passed in
int capacity = computeArrayListCapacity(elements.length);
ArrayList<E> list = new ArrayList<>(capacity);
Collections.addAll(list, elements);
return list;
}
这里可以看到newArrayList
可以 看到Collections.addAll(list, elements);
public static <T> boolean addAll(Collection<? super T> c, T... elements) {
boolean result = false;
for (T element : elements)
result |= c.add(element);
return result;
}
addAll
方法接收的入参Collection<? super T>
,所以你给给这方法传任意只要是Fruit
子类或者他自身的类型的数据,因此你可以往里面丢香蕉、苹果。
参考:https://www.zhihu.com/question/20400700/answer/117464182
http://swiftlet.net/archives/1950
http://swiftlet.net/archives/1950
《Java核心技术卷1》