9.3.2 设定类型通配符的上限
9.3.2 设定类型通配符的上限
当直接使用List<?>
这种形式时,即表明这个List
集合可以是任何泛型List
的父类。但还有一种特殊的情形,程序不希望这个List<?>
是任何泛型List
的父类,只希望它代表某一类泛型List
的父类。考虑一个简单的绘图程序,下面先定义三个形状类
1 | // 定义一个抽象类Shape |
1 | // 定义Shape的子类Circle |
1 | // 定义Shape的子类Rectangle |
上面定义了三个形状类,其中Shape
是一个抽象父类,该抽象父类有两个子类: Circle
和Rectangle
。接下来定义一个Canvas
类,该画布类可以画数量不等的形状( Shape
子类的对象),那应该如何定义这个Canvas
类呢?考虑如下的Canvas
实现类。
1 | import java.util.*; |
注意上面的drawAll()
方法的形参类型是List<Shape>
,而List<Circle>
并不是List<Shape>
的子类型,因此,下面代码将引起编译错误。
1 | List<Circle> circleList = new ArrayList<Circle>(); |
1 | Erasure of method drawAll(List<Shape>) is the same as another method in type Canvas |
关键在于List<Circle>
并不是List<Shape>
的子类型,所以不能把List<Circle>
对象当成List<Shape>
使用。为了表示List<Circle>
的父类,可以考虑使用List<?>
,但此时从List<?>
集合中取出的元素只能被编译器当成Object
处理。为了表示List
集合的所有元素是Shape
的子类,Java
泛型提供了被限制的泛型通配符。被限制的泛型通配符表示如下
1 | //它表示泛型形参必须是Shape子类的List |
有了这种被限制的泛型通配符,就可以把上面的Canvas
程序改为如下形式
1 | // 同时在画布上绘制多个形状,使用被限制的泛型通配符 |
将Canvas
改为如上形式,就可以把List<Circle>
对象当成List<? extends Shape>
使用。即**List<? extends Shape>
可以表示List<Circle
、List<Rectangle>
的父类—只要List
后尖括号里的类型是Shape
的子类型即可。List<? extends Shape>
是受限制通配符的例子,此处的问号(?
)代表一个未知的类型,就像前面看到的通配符一样。但是此处的这个未知类型一定是Shape
的子类型(也可以是Shape
本身),因此可以把Shape
称为这个通配符的上限**(upper bound
)。
类似地,由于程序无法确定这个受限制的通配符的具体类型,所以不能把Shape
对象或其子类的对象加入这个泛型集合中。例如,下面代码就是错误的。
1 | public void addRectangle(List<? extends Shape> shapes){ |
与使用普通通配符相似的是, shapes.add()
的第二个参数类型是? extends Shape
,它表示Shape
未知的子类,程序无法确定这个类型是什么,所以无法将任何对象添加到这种集合中。
指定通配符上限的集合只能取出元素
简而言之,这种指定通配符上限的集合,只能从集合中取元素(取出的元素总是上限的类型),不能向集合中添加元素(因为编译器没法确定集合元素实际是哪种子类型)。
对于更广泛的泛型类来说,指定通配符上限就是为了支持类型型变。
比如Son
是Father
的子类,这样A<Father>
就相当于A<? extends Son>
的子类,**可以将A<Father>
赋值给A<? extends Son>
类型的变量,这种型变方式被称为协变
**。
反过来了
协变只出不进
对于协变的泛型类来说,它只能调用泛型类型作为返回值类型的方法(编译器会将该方法返回值当成通配符上限的类型);而不能调用泛型类型作为参数的方法。口诀是:协变只出不进!
对于指定通配符上限的泛型类,相当于通配符上限是Object.