为何要使用泛型通配符
根据里氏替换原则,我们通常是用基类的引用指向子类对象,例如
class Shape {}
class Circle extends Shape {}
class Square extends Shape {}
Shape shape = new Circle();
对于泛型来说,下面的代码是错误的
// 不支持这种泛型语法
List<Shape> shapes = new ArrayList<Cirlce>();
这种泛型语法被定义为错误的语法。由于泛型擦除,在运行时,系统并不知道 shapes
保存的对象的类型,那何谈确保添加正确类型的对象呢?
泛型通配符上界
如果我们仍然想建立这种关系,可以使用泛型通配符,由问号表示
List<? extends Shape> shapes = new ArrayList<Circle>();
? extends Shape
定义了泛型的下界,也就是 Shape 类或者它的子类,但是我们似乎并不关心是什么子类型,这里我们只想要把 shapes
指向一个泛型类型为 Circle 的 ArrayList 对象。
但是由于泛型擦除,编译期和运行期,都无法知道 shapes
到底保存了哪个类型,那么怎么能安全地向其中添加对象呢? 因此Java拒绝这种做法。例如下面代码是错误的
List<? extends Shape> shapes = new ArrayList<Circle>();
// 编译报错
shapes.add(new Shape());
这个错误我们完全可以解释的,add 方法的原型如下
boolean add(T t);
add 方法的参数类型是一个泛型,在编译期,编译器会在这里添加一个边界动作,也就是检测类型是否正确。 然而,此时编译器只知道泛型的类型为 ? extends Shape
,但是并不知道具体是哪个类型,那么就无法确保添加对象的类型正确。 因此这种语法被拒绝了。
List<? extends Shape> shapes
连对象的无法添加,那么它存在的意义是什么呢? 由于 List<? extends Shape>
像是一个基类,因此用在方法的参数类型
boolean isExists(List<? extends Shape> list, Shape obj) {
list.foreach(System.out::println);
return list.contains(obj);
}
从这个方法可以看出,我们可以对List<? extends Shape> list
进行遍历,这应该是理所当然的,但是遍历对象的类型只能是 Shape。
然而居然可以对 List<? extends Shape> list
调用 contains 方法,我们刚在前面说过不能调用 add 方法,为何? 可以看下 contains 方法的原型
boolean contains(Object obj);
原来它的参数类型是 Object,这样就不用编译器来确保类型的正确, 因此List<? extends Shape> list
调用 contains 方法,也是理所当然的。
泛型通配符下界
? extends Shape
表示了泛型通配符的的上界是 Shape, 而 ? super Cirlce
表示的泛型通配符的下界是 Circle 的基类。因此可以向这个集合中添加元素
注意,泛型类的泛型参数不能使用
<T super MyClass>
这种形式,但是在泛型类的内部可以使用 <? super T>。
List<? super Circle> circles = new ArrayList<>();
circles.add(new Circle();
? super Circle
只是指定了下界,由于泛型擦除,我们并不知道具体保存了什么类型,所以从 List<? super Circle> circles
获取的元素只能是 Object 类型,如下
List<? super Circle> circles = new ArrayList<>();
circles.add(new Circle()
// 获取的元素只能是 Object 类型
Object obj = circles.get(0);