目录
29、30两条主要介绍的是利用泛型扩展类和方法的使用范围,而使用通配符类型将进一步扩大两者的使用灵活性,通配符类型的本质是将原本只能指定特定类型的泛型推广为指定一个类型的范围。
泛型可变参数则是28条介绍的泛型数组的一个特例,是Java设计者为了保存泛型可变参数这个便利工具的情况下做出的一定妥协,但是使用这个工具就需要程序员进行一定的安全性检验,否则就会出现问题。
有限通配符类型
一般来说,泛型方法的输入参数是向下兼容的,比如public void push(E e),如果指定E为Number,他自然可以兼容其子类型Integer,但是这对于泛型接口是不适用的(List<String>并不是List<Object>的子类型),当我们想让某一个方法可以统一处理一类泛型数据类型,就需要使用到有限制通配符类型(我们在说原生态类型的时候也提到过无限制通配符类型,它是原生态类型的一个理想替代,允许指向任何一种List实例,同时保证了类型安全,但是不允许往无限通配符类型的集合当中写入元素,因为编译器并不知道写入的元素是否符合要求)。
public void pushAll(List<? extends E> list){...};//有限通配符的泛型接口
有限通配符类型的应用准则PECS法则
PECS法则的全称是Producer-extends, Consumer-super,意思是如果方法的输入是作为生成者则该参数对应类型应向下兼容,如果方法的输入是作为消费者则应向上兼容。首先,我们先来了解一下什么是生产者,什么是消费者。
- 生产者是向方法提供数据的,它被方法内的组件所调用。
- 消费者是应用方法内各组件所提供的数据,它会调用方法内的组件。
知道这个概念以后,就比较好理解了,因为生产者是提供数据对外使用的,所以只能提供向下兼容的类型,而消费者要能够使用方法内的某一实例,则需要向上兼容才能保证可以安全应用。
public void pushAll(Collection<? extends E> list){
for(E e: list){
push(e)
}
};//这里list作为数据供应方为push方法提供元素,是生产者
public void popAll(Collection<? super E> dst){
while(!isEmpty()){
dst.add(pop());
}
};//这里的dst消费了pop()返回的元素,是消费者
Java编译器的类型推导
对于有限通配符的使用,细想会有一个问题,就是Java编译器是如何知道输入数据类型的。这就涉及到Java编译器的类型推导,如果输入的参数是唯一的,那可以直接根据该输入的声明进行推测:
public class Test{
public static void main(String[] args){
String input = "hello";
Input(input);
}
static <T> void Input(T input){
System.out.println("Input is "+input+" the class is "+input.getClass());
}
}
// Input is hello the class is java.lang.String
但是如果有多个不同类型的输入参数,Java编译器就会根据多个不同输入参数的最顶级类型进行推测(比如:两个参数一个是Integer,一个是Double,编译器就会推测是Number):
import java.util.Set;
import java.util.HashSet;
public class Test{
public static void main(String[] args){
Set<Integer> set1 = Set.of(1, 2, 3);
Set<Double> set2 = Set.of(1.0, 2.0, 3.0);
Set<Number> result = union(set1,set2); //推测T是Number类型
}
static <T> Set<T> union(Set<? extends T> set1, Set<? extends T> set2){
Set<T> result = new HashSet<>(set1);
result.addAll(set2);
return result;
}
}
特殊的消费者Comparable接口
单独列一个标题,是想让大家记住,Comparable接口是一个消费者,因为其调用的compareTo方法会应用被对比因素,因此在使用时Comparable<? super E>要始终优于Comparable<E>(允许元素与其父类进行对比)。
public static <? extends Comparable<? super E>> E max(List<? extends E> list)
这里就实现了一个方法能够传回list中的最大值,其中对比类型E继承了Comparable接口(这是必须的),其次E的对比类型从本身扩展到了其父类。
类型参数与通配符的对比
这里重点对比的是两种方法的好坏:
public static <E> void swap(List<E> list, int i, int j);//用类型参数实现的泛型方法
public static void swap(List<?> list, int i, int j);//用通配符实现的同等方法
作者认为作为公共API来说,第二种方法更优,因为更简单。一般来说,如果类型参数只在方法声明中出现(不在类声明中出现),就可以使用通配符替代。
但是第二种方式有一个问题,list无法添加任何除null以外的值,所以单纯的通过get和set方法是无法实现这个方法的,那就需要一个swapHelper方法来辅助:
public static <E> void swapHelper(List<E> list, int i, int j){
list.set(i, list.set(j, list.get(i)));
};//辅助方法
public static void swap(List<?> list, int i, int j){
swapHelper(list, i, j);
};
其实就是把第一个方法作为第二个方法的辅助,但是方法签名依然保留了第二种方法。
泛型可变参数
可变参数本质就是通过一个数组,将任意数量的输入装入这个数组后提供给方法。因为第28条有提过泛型数组本身是非法的,那为什么会允许泛型可变参数存在呢?因为Java的设计者对于泛型可变参数的便利性做出了让步,但是依然对于所有的泛型可变参数都做出了告警,与泛型数组一样这里的风险主要来自于潜在的堆污染问题:
static void dangerous(List<Integer>...IntegerList){
List<String> strings = Arrays.asList("Hello", "world");
Object[] objects = IntegerList;
Object[0] = strings; //Heap pollution
}
因为所有的泛型信息在编译器中都会被擦除,因此要向数组中写入数据的时候是没有有效的类型检验的。因此,如果程序员们坚持要使用这个工具,就要自行的去排出这个风险。作者强调了只要泛型可变参数符合了以下两个原则,就可以被是做类型安全的:
- 泛型数组在方法中不会被插入或者修改任何值
- 方法不允许对数组的引用转义(这里的转义应该是将数组的引用泄漏到了方法外部,导致对外部开放了该数组),同时不允许另一个方法访问一个泛型可变参数数组,除非这个方法也是用SafeVarargs注解过的,或者只是计算数组内容部分。
static <T> T[] toArray(T... args){
return args
}//这里直接将泛型数组args的引用暴露给了外部
只要符合这两个原则,就可以放心使用泛型可变参数,通过SafeVarargs注解将告警消除。