9.3.3 设定类型通配符的下限

9.3.3 设定类型通配符的下限

除可以指定通配符的上限之外,Java也允许指定通配符的下限,通配符的下限用<? super 类型>的方式来指定,通配符下限的作用与通配符上限的作用恰好相反。

逆变

指定通配符的下限就是为了支持类型型变。比如SonFather的子类,当程序需要一个A<? super Father>变量时,程序可以将A<Father>A<Object>赋值给A<? super Father>类型的变量,这种型变方式被称为逆变

对于逆变的泛型集合来说,编译器只知道集合元素是下限的父类型,但具体是哪种父类型则不确定。因此,这种逆变的泛型集合能向其中添加元素(因为实际赋值的集合元素总是逆变声明的父类),从集合中取元素时只能被当成Object类型处理(编译器无法确定取出的到底是哪个父类的对象)。

假设自己实现一个工具方法:实现src集合中的元素复制到dest集合的功能,因为dest集合可以保存src集合中的所有元素,所以dest集合元素的类型应该是sre集合元素类型的父类

对于上面的copy方法,可以这样理解两个集合参数之间的依赖关系:不管src集合元素的类型是什么,只要dest集合元素的类型与前者相同或者是前者的父类即可,此时通配符的下限就有了用武之地。下面程序采用通配符下限的方式来实现该copy方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.util.*;

public class MyUtils {
// 下面dest集合元素类型必须与src集合元素类型相同,或是其父类
public static <T> T copy(List<? super T> dest, List<T> src) {
T last = null;
for (T ele : src) {
last = ele;
// 逆变的泛型集合添加元素是安全的
dest.add(ele);
}
return last;
}

public static void main(String[] args) {
List<Number> listNumber = new ArrayList<>();
List<Integer> listInteger = new ArrayList<>();
listInteger.add(5);
// 此处可准确的知道最后一个被复制的元素是Integer类型
// 与src集合元素的类型相同
Integer last = copy(listNumber, listInteger); // 代码1
System.out.println(listNumber);
}
}

使用这种语句,就可以保证程序的代码1 处调用后推断出最后一个被复制的元素类型是Integer,而不是笼统的Number类型。

上面方法用到了泛型方法的语法,就是在方法修饰符和返回值类型之间用<>定义泛型形参。关于泛型方法更详细介绍可参考下一节。

实际上,Java集合框架中的TreeSet<E>有一个构造器也用到了这种设定通配符下限的语法,如下所示:

1
2
3
4
5
6
7
8
9
package java.util;
public class TreeSet<E> extends AbstractSet<E>
implements NavigableSet<E>, Cloneable, java.io.Serializable {
...
public TreeSet(Comparator<? super E> comparator) {
this(new TreeMap<>(comparator));
}
...
}

正如前一章所介绍的, TreeSet会对集合中的元素按自然顺序定制顺序进行排序。如果需要TreeSet对集合中的所有元素进行定制排序,则要求TreeSet对象有一个与之关联的Comparator对象。上面构造器中的参数comparator就是进行定制排序的Comparator对象。

Comparator接口也是一个带泛型声明的接口

1
2
3
4
5
6
...
@FunctionalInterfacepublic
interface Comparator<T> {
int compare(T o1, T o2);
...
}

通过这种带下限的通配符的语法,可以在创建TreeSet对象时灵活地选择合适的Comparator。假定需要创建一个TreeSet<String>集合,并传入一个可以比较String大小的Comparator,这个Comparator既可以是Comparator<String>,也可以是Comparator<Object>——只要尖括号里传入的类型是String的父类型(或它本身)即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.util.*;

public class TreeSetTest {
public static void main(String[] args) {
// Comparator的实际类型是TreeSet的元素类型的父类,满足要求
TreeSet<String> ts1 = new TreeSet<>(new Comparator<Object>() {
public int compare(Object fst, Object snd) {
return hashCode() > snd.hashCode() ? 1 : hashCode() < snd.hashCode() ? -1 : 0;
}
});
ts1.add("hello");
ts1.add("wa");
// Comparator的实际类型是TreeSet元素的类型,满足要求
TreeSet<String> ts2 = new TreeSet<>(new Comparator<String>() {
public int compare(String first, String second) {
return first.length() > second.length() ? -1 : first.length() < second.length() ? 1 : 0;
}
});
ts2.add("hello");
ts2.add("wa");
System.out.println(ts1);
System.out.println(ts2);
}
}

通过使用这种通配符下限的方式来定义TreeSet构造器的参数,就可以将所有可用的Comparator作为参数传入,从而增加了程序的灵活性。当然,不仅TreeSet有这种用法, TreeMap也有类似的用法,具体的请查阅JavaAPI文档。