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程序中的drawAll方法:

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

改为如下形式:

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

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
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>类型的变量,这种型变方式被称为协变

https://blog.csdn.net/u010900754/article/details/101113667
什么是协变?协变其实指的是,如果基础类型具备父子关系,那么对应的容器类型也具备。
泛型不允许协变,而数组允许协变
数组的报错是在存元素时抛出的,而泛型的报错是在取元素是抛出的,这样,泛型的报错时机就非常延后了,如果类型不对,压根就不应该让这个元素放入,否则,就只能在读取时进行强转才能发现,可别小看这个时机问题,一旦发生,非常难定位,很难查到是在哪里放入了类型异常的元素,所以泛型不允许协变。原因就是,类型转换的问题需要延后到读取时才能发现。而数组则可以在存入时就检测到类型不匹配的问题,从而fail-fast。

https://blog.csdn.net/qq_37779333/article/details/113897799
在面向对象程序设计中,协变返回类型指的是子类中的成员函数的返回值类型不必严格等同于父类中被重写的成员函数的返回值类型,而可以是更”狭窄”的类型
Java 5.0添加了对协变返回类型的支持,即子类覆盖(即重写)基类方法时,返回的类型可以是基类方法返回类型的子类。协变返回类型允许返回更为具体的类型

https://www.cnblogs.com/stevenshen123/p/9215750.html

  • ? extends 对应 协变,
  • ? super 对应 逆变。

协变只出不进

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