9.3.2 设定类型通配符的上限

9.3.2 设定类型通配符的上限

当直接使用List<?>这种形式时,即表明这个List集合可以是任何泛型List的父类。但还有一种特殊的情形,程序不希望这个List<?>是任何泛型List的父类,只希望它代表某一类泛型List的父类。考虑一个简单的绘图程序,下面先定义三个形状类

1
2
3
4
5
// 定义一个抽象类Shape
public abstract class Shape
{
public abstract void draw(Canvas c);
}
1
2
3
4
5
6
7
8
9
// 定义Shape的子类Circle
public class Circle extends Shape
{
// 实现画图方法,以打印字符串来模拟画图方法实现
public void draw(Canvas c)
{
System.out.println("在画布" + c + "上画一个圆");
}
}
1
2
3
4
5
6
7
8
9
// 定义Shape的子类Rectangle
public class Rectangle extends Shape
{
// 实现画图方法,以打印字符串来模拟画图方法实现
public void draw(Canvas c)
{
System.out.println("把一个矩形画在画布" + c + "上");
}
}

上面定义了三个形状类,其中Shape是一个抽象父类,该抽象父类有两个子类: CircleRectangle。接下来定义一个Canvas类,该画布类可以画数量不等的形状( Shape子类的对象),那应该如何定义这个Canvas类呢?考虑如下的Canvas实现类。

1
2
3
4
5
6
7
8
9
10
import java.util.*;

public class Canvas {
// 同时在画布上绘制多个形状
public void drawAll(List<Shape> shapes) {
for (Shape s : shapes) {
s.draw(this);
}
}
}

注意上面的drawAll()方法的形参类型是List<Shape>,而List<Circle>并不是List<Shape>的子类型,因此,下面代码将引起编译错误。

1
2
3
4
5
List<Circle> circleList = new ArrayList<Circle>();
Canvas c = new Canvas();
// 由于List<Circle>并不是List<Shape>的子类型,
// 所以下面代码引发编译错误
c.drawAll(circleList);
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
2
//它表示泛型形参必须是Shape子类的List
List<? extends Shape>

有了这种被限制的泛型通配符,就可以把上面的Canvas程序改为如下形式

1
2
3
4
5
6
// 同时在画布上绘制多个形状,使用被限制的泛型通配符
public void drawAll(List<? extends Shape> shapes) {
for (Shape s : shapes) {
s.draw(this);
}
}

Canvas改为如上形式,就可以把List<Circle>对象当成List<? extends Shape>使用。即**List<? extends Shape>可以表示List<CircleList<Rectangle>的父类—只要List后尖括号里的类型是Shape的子类型即可。
List<? extends Shape>是受限制通配符的例子,此处的问号(?)代表一个未知的类型,就像前面看到的通配符一样。但是此处的这个未知类型一定是Shape的子类型(也可以是Shape本身),因此可以把Shape称为这个
通配符的上限**(upper bound)。

类似地,由于程序无法确定这个受限制的通配符的具体类型,所以不能把Shape对象或其子类的对象加入这个泛型集合中。例如,下面代码就是错误的。

1
2
3
4
public void addRectangle(List<? extends Shape> shapes){
//下面代码引起编译错误
shapes.add(0, new Rectangle());
}

与使用普通通配符相似的是, shapes.add()的第二个参数类型是? extends Shape,它表示Shape未知的子类,程序无法确定这个类型是什么,所以无法将任何对象添加到这种集合中。

指定通配符上限的集合只能取出元素

简而言之,这种指定通配符上限的集合,只能从集合中取元素(取出的元素总是上限的类型),不能向集合中添加元素(因为编译器没法确定集合元素实际是哪种子类型)。
对于更广泛的泛型类来说,指定通配符上限就是为了支持类型型变
比如SonFather的子类,这样A<Father>就相当于A<? extends Son>的子类,**可以将A<Father>赋值给A<? extends Son>类型的变量,这种型变方式被称为协变**。
反过来了

协变只出不进

对于协变的泛型类来说,它只能调用泛型类型作为返回值类型的方法(编译器会将该方法返回值当成通配符上限的类型);而不能调用泛型类型作为参数的方法。口诀是:协变只出不进!
对于指定通配符上限的泛型类,相当于通配符上限是Object.