在泛型概述-基本概念当中,我们介绍了有关类型参数限定的概念,使用 extends
关键字,给类型参数加以限定,例如:<T extends Fruit>
,它表示 Fruit
或者 Fruit
的子类型。
public class Plate<T extends Fruit> {
//......省略部分代码
public void addFruits(Plate<T> plate) {
for (T fruit : plate.getFruitList()) {
fruitList.add(fruit);
}
}
}
在 Plate
类中,我们添加了一个方法 addFruits(Plate<T> plate)
,此方法的作用是将参数 plate,添加到当前的 Plate 对象当中。
List<Apple> appleList = new ArrayList<>();
Apple apple1 = new Apple("apple1");
Apple apple2 = new Apple("apple2");
appleList.add(apple1);
appleList.add(apple2);
Plate<Apple> applePlate = new Plate<>(appleList);
Plate<Fruit> fruitPlate = new Plate<>();
fruitPlate.addFruits(applePlate);
我们构造了一个苹果盘子 applePlate,并将它作为参数传递给 fruitPlate 的 addFruits() 方法,从实际生活的角度中来看,这样的需求是没有任何问题的,但是这么写的话,编译器会报错。因为 Plate<Fruit>
中的 addFruits(Plate<Fruit> plate)
的方法参数接收的是一个 Plate<Fruit>
类型的对象,而我们传递的确是一个 Plate<Apple>
类型的对象。
这个时候我们就需要对类型参数加以限定,来对 addFruits
改造一下:
public <E extends T> void addFruits(Plate<E> plate) {
for (T fruit : plate.getFruitList()) {
fruitList.add(fruit);
}
}
我们对 addFruits
方法能够接收的参数类型进行限定,它能够接受的类型必须是 T
或者是 T
的子类型,这个时候我们上面的代码就可以正常运行了。
通配符
我们可以用一种更简单的,带子类型限定的通配符类型来替换上面的泛型方法。也就是将
public <E extends T> void addFruits(Plate<E> plate)
替换成 public void addFruits(Plate<? extends T> plate)
子类型限定
<? extends T>
,称为子类型限定的通配符类型,它表示 T
以及 T
的任意子类型。采用通配符形式的写法,无疑看上去更简单明了。
这里需要注意的是,通配符是用来实例化定义好的类型参数的。如果一个类或者一个方法并不是泛型类或者泛型方法,那我们是没有办法使用通配符的。
public class GenericType<?> {
? type;
public ? getType() {
return type;
}
public void setType(? type) {
this.type=type;
}
}
GenericType 这种写法是不支持的,可以看到,没有办法使用通配符类型来定义类,声明属性,也没有办法作为方法的返回类型。
子类型限定的通配符也有它的局限性
List<Apple> appleList = new ArrayList<>();
Apple apple1 = new Apple("apple1");
Apple apple2 = new Apple("apple2");
appleList.add(apple1);
appleList.add(apple2);
List<? extends Fruit> fruitList = appleList;
for (Fruit fruit : fruitList) {
System.out.println(fruit.getName());
}
使用了子类型限定通配符 <? extends Fruit>
,它表示 Fruit
以及 Fruit
的任意子类型。所以我们可以将 List<Apple>
类型的对象 appleList 赋值给它。还记得在泛型概述-基本概念当中有讲过,泛型是不支持协变的,但是使用了这种子类型限定通配符类型之后,它就符合了协变的规则。
List<Apple> appleList = new ArrayList<>();
Apple apple1 = new Apple("apple1");
Apple apple2 = new Apple("apple2");
appleList.add(apple1);
appleList.add(apple2);
List<? extends Fruit> fruitList = appleList;
fruitList.add(new Fruit("fruit"));//compile error
fruitList.add(new Apple("apple"));//compile error
fruitList.add(new Banana("banana"));//compile error
我们可以看一下 ArrayList 类中的 add()
和 get()
两个方法
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
我们声明了一个 List<? extends Fruit>
类型的对象 fruitList,那么 add 方法和get 方法中的类型参数就变成了 ?extends Fruit 类型
public boolean add(? extends Fruit e) {
// 可以认为变成了这种形式,实际上没有办法这么编写
}
public ? extends Fruit get(int index) {
// 可以认为变成了这种形式,实际上没有办法这么编写
}
当我们打印水果名称的时候,也就是从 List<? extends Fruit>
类型的 fruitList 对象当中读取数据的时候是没有问题的,get()
方法返回的是 ? extends Fruit
类型,不论它是什么类型,一定可以向上转型成 Fruit 类型;但是当我们需要调用 add()
方法的时候,编译器就会提示错误,这是因为 add()
方法的参数是 <? extends Fruit>
类型,它代表的是 Fruit
以及 Fruit
的任意子类型,并不能够知道具体是什么类型,所以就不能够调用 add()
方法。add(null)
除外🤢
假设我们允许调用 add()
方法的话,就会出现问题
List<? extends Fruit> fruitList = appleList;
fruitList.add(new Fruit("fruit"));//假设没有问题
fruitList.add(new Apple("apple"));//假设没有问题
fruitList.add(new Banana("banana"));//假设没有问题
这就相当于是向 List<Apple>
类型的 appleList 对象中,添加了 Fruit
,Apple
,Banana
三种类型的对象,先不说它违背了泛型的类型安全的原则,这等于是埋下了一颗定时炸弹,在我们从 appleList
对象当中取数据的时候,就有可能发生类型转换异常。
fruitList.add(new Apple("apple"));
细心的朋友可能发现了,即使是向 fruitList 当中添加 Apple
类型的对象也是不可以的,上面说了 add 方法 的参数类型是 ? extends Fruit 对于编译器来说它会将 ? extends Fruit 识别为 CAP#1 extends Fruit 类型,它没有办法匹配到 CAP#1的具体类型是什么,所以没有办法进行赋值。
无限定通配符
<?>
称为无限定通配符,当一些操作与具体的类型无关的时候,或者说我们不需要知道类型信息的时候,就可以使用无限定的通配符类型,来实例化我们定义的类型参数,例如交换数组中的元素,比较元素的大小,获取元素的个数等等。
例如我们 Collections
工具类中提供的 swap 方法,用于交换列表中指定位置的元素,这个时候不需要知道具体的类型,可以使用通配符类型。
public static void swap(List<?> list, int i, int j) {
// instead of using a raw type here, it's possible to capture
// the wildcard but it will require a call to a supplementary
// private method
final List l = list;
l.set(i, l.set(j, l.get(i)));
}
除了使用原生类型之外,我们还可以通过一个辅助的私有方法来匹配通配符的类型
public static void swap(List<?> list, int i, int j) {
swapHelp(list, i, j);
}
private static <T> void swapHelp(List<T> list, int i, int j) {
T temp = list.get(i);
list.set(i, list.get(j));
list.set(j, temp);
}
对于编译器来说它会将 ? 类型识别为 CAP#1 类型,在内部会调用 swapHelp 方法,编译器会将推断出 T
的类型就是 CAP#1,之后就像是普通的容器类一样进行读写操作。
上面出现的关于通配符的例子,都可以使用泛型方法替代:
public static void swap(List<?> list, int i, int j)
它的泛型方法形式是
public static <T> void swap(List<T> list, int i, int j)
看到这里你可能有些疑惑,因为 swap 方法的泛型方法形式和 swapHelp 方法的形式是一模一样的,你可能会觉得这么写有一些多此一举,采用通配符的形式简洁明了,减少了参数类型的个数,所以应该尽可能的采用通配符类型。
并不是所有的泛型方法都可以改写成通配符的形式,在这里我直接引用老马说编程中的两个例子来说明:
参数类型间具有依赖关系
public static <D,S extends D> void copy(List<D> dest,
List<S> src){
for(int i=0; i<src.size(); i++){
dest.add(src.get(i));
}
}
我们只能将其简化成具有一个参数类型的形式
public static <D> void copy(List<D> dest,
List<? extends D> src){
for(int i=0; i<src.size(); i++){
dest.add(src.get(i));
}
}
返回值依赖参数类型
public static <T extends Comparable<T>> T max(List<T> arr){
T max = arr.get(0);
for(int i=1; i<arr.size(); i++){
if(arr.get(i).compareTo(max)>0){
max = arr.get(i);
}
}
return max;
}
max
方法的返回值是依赖于参数类型 T
的,也没有办法将其改写为通配符形式。
超类型限定通配符
上面我们说到子类限定的通配符和无限定的通配符,两种类型的通配符,这两种通配符类型都有一个缺点,就是没有办法进行写操作。
对于 List<?>
来说,我们获取的元素只能赋值给 Object
类型的引用,并且没有办法进行 add 操作,除了 null
值。
对于 List<? extends T>
来说,我们获取的元素只能赋值给它的上限 T
类型,同样没有办法进行 add 操作,除了 null
值。
现在我们有这样一个需求,需要将苹果盘子中的水果,全部放到另一个水果盘子中区,也就是将 Plate<Apple>
中的所有水果,复制一份到 Plate<Fruit>
当中,这个需求没有什么问题,我们在 Plate<T extends Fruit>
类中加入一个方法:
public void copyTo(Plate<T> dest) {
List<T> destList = dest.getFruitList();
for (T t : fruitList) {
destList.add(t);
}
}
public static void main(String[] args) {
List<Apple> appleList = new ArrayList<>();
Apple apple1 = new Apple("apple1");
Apple apple2 = new Apple("apple2");
appleList.add(apple1);
appleList.add(apple2);
Plate<Apple> applePlate = new Plate<>(appleList);
Plate<Fruit> fruitPlate = new Plate<>();
applePlate.copyTo(fruitPlate);
for (Fruit fruit : fruitPlate.getFruitList()) {
System.out.println(fruit.getName());
}
}
编译器会在这一行 applePlate.copyTo(fruitPlate);
提示错误,copyTo 方法需要 Plate<Apple>
类型,但是我们传递的是 Plate<Fruit>
类型,这个时候就需要用到超类型限定的通配符:<? super T>
public void copyTo(Plate<? super T> dest) {
List<? super T> destList = dest.getFruitList();
for (T t : fruitList) {
destList.add(t);
}
将 copyTo 方法改成这个样子之后,就可以正常编译运行了,对于 Plate<Apple>
的类型来说,它的 copyTo
方法接收的参数类型是 Plate<? super Apple>
类型,意思是 Apple
或者 Apple
的任意父类型,这个时候我们就可以将 Plate<Fruit>
类型的对象传递给该方法了。
另外超类型限定通配符允许写入,详细看一下 copyTo
方法中的代码,可以发现我们调用 destList.add(t)
方法将 T
类型的对象写入到了 List<? super T>
的列表中,对于 <? super T>
而言,仅仅能够写入 T
或者 T
类型的子类型。
除了可以灵活的写入之外,超类型限定的通配符类型还可以应用于实现 Comparable
接口
public class Fruit implements Comparable<Fruit> {
private int weight;
//... 省略部分代码
@Override
public int compareTo(Fruit fruit) {
if (weight == fruit.getWeight()) {
return 0;
} else if (weight > fruit.getWeight()) {
return 1;
} else {
return -1;
}
}
}
让 Fruit
实现 Comparable<Fruit>
接口,根据水果的重量 weight 的大小来作为比较规则
现在我们需要取出 Plate<T extends Fruit>
对象中重量最大的水果,代码如下:
public class PlateUtils {
public static <T extends Comparable<T>> T max(List<T> fruitList) {
Iterator<T> i = fruitList.iterator();
T candidate = i.next();
while (i.hasNext()) {
T next = i.next();
if (next.compareTo(candidate) > 0)
candidate = next;
}
return candidate;
}
}
List<Fruit> fruitList = new ArrayList<>();
Fruit fruit1 = new Fruit("fruit1");
fruit1.setWeight(2);
Fruit fruit2 = new Fruit("fruit2");
fruit2.setWeight(10);
fruitList.add(fruit1);
fruitList.add(fruit2);
Plate<Fruit> fruitPlate = new Plate<>(fruitList);
Fruit maxFruit = PlateUtils.max(fruitPlate.getFruitList());
System.out.println(maxFruit.getName());
//print:fruit2
似乎没有问题,但是如果我们将 List<Apple>
传递进去的话,编译器就会报错
public class Apple extends Fruit {
public Apple(String name) {
super(name);
}
}
这是由于我们的 Apple 虽然继承了 Fruit 但是并没有实现 Comparable<Apple>
接口,对于 max 方法来说,它会根据参数推断出 T
的实际类型为 Apple 类型,Apple 实现的是 Comparable<Fruit>
接口,然而需要的是 Comparable<Apple>
类型,类型不匹配,所以编译器报错。
对于 Apple 来说,它并不需要重新实现 Comparable<Apple>
接口,Fruit 类中实现的 compareTo 规则已经适用于它了,这个时候超类型限定通配符就派上用场了:
public static <T extends Comparable<? super T>> T max(List<T> fruitList) {
Iterator<T> i = fruitList.iterator();
T candidate = i.next();
while (i.hasNext()) {
T next = i.next();
if (next.compareTo(candidate) > 0)
candidate = next;
}
return candidate;
}
我们修改了 T
类型的限定,限制它实现的 Comparable
接口类型必须是 T
或者 T
的超类型,对于 Apple
来说,它实现的是 Comparable<Fruit>
类型,符合要求。