1. 解决的问题
在以往对泛型认知中,很容易知道泛型编译后类型就确定了下来。类似:
public class Food {}
class Fruit extends Food {}
class Apple extends Fruit {}
class Orange extends Fruit {}
List<Fruit> fruitList = new ArrayList<>();
那么,如果右边的泛型遇到了Fruit具体的子类时会发生什么呢?
List<Fruit> fruitList = new ArrayList<Apple>();
会提示编译错误!这就有很大的限制了,很多场景下,我们真的需要能有一个接受类似Fruit和它子类的泛型列表。Sun团队就推出了通配符来解决这个问题。它表示左边用来接收的变量不单单是当前类,还可能是基类或者子类,不再是一种确定类型,而是一条继承链上的所有类型。
List<? extends Fruit> fruitList = new ArrayList<Apple>();
这时候就已经没有任何报错了,这就是上界通配符<? extends T>,它确立了继承的顶点是T,同时接收T的子类。
那么下限通配符<? super T>呢?
List<? super Fruit> fruitList = new ArrayList<Food>();
下限通配符自然是确定了下限,确定了T是继承链最后的节点,同时接收T的基类。
2. 上界通配符与下限通配符的add()和get()
在编程思想中,取舍贯穿了始终。我们使得列表同时能够接受不同的类型在列表中,就必然会使得原本的泛型出现一些问题。通配符的出现违背了原有的泛型擦除即确定类型的机制,它仅仅代表一个范围,一条继承链上的类型范围。
2.1 上界通配符
那么,在泛型擦除的时候,上界通配符就无法确定具体的类型,它有可能是当前类或者子类。根据向上转型的机制,基类无法转换成子类。而上界通配符允许出现实例化子类,例如:List<? extend T> list = new ArrayList<T的子类>(),所以这就是上界通配符无法 add()的原因 ,即无法添加任何元素,因为违背了向上转型的原则。而且,从 <? extend T> 中 get() 的值的类型必须是T,也是由于向上转型的限制。
2.2 下限通配符
下限通配符则符合向上转型的机制,即使T可能是T本身或者它的基类,但是T都能接受T及T的子类。所以,add() 不受影响。但是,由于最终的类型不确定,get() 的时候,类型只能是Object,需要的类型必须强转。
2.3 总结
可以看到,上界通配符和下限通配符两者的限制均会受到实例化对象的限制,实例化对象决定他们能够容纳的类型范围,所以使用的时候务必根据业务谨慎选择,避免出现生产问题。