Java 泛型提供编译期类型检查,同时保持与泛型出现之前字节码的兼容性。
编译器会擦除类型参数,在需要的位置插入强制类型转换,并可能生成桥接方法,
以便擦除后仍能保留重写关系。因此,List<String> 和 List<Integer> 具有相同的运行时类,
并且无法可靠地从对象中恢复泛型类型参数。
不变性和边界 #
泛型类是不变的:List<Integer> 不是 List<Number>,因为后者允许插入 Double。
通配符在 API 边界表达变型。
static double sum(List<? extends Number> values) {
return values.stream().mapToDouble(Number::doubleValue).sum();
}
static void addDefaults(List<? super Integer> destination) {
destination.add(0);
}使用 extends 表示从中读取的生产者,使用 super 表示向其中写入的消费者。
List<? extends Number> 可以产生 Number 值,但不接受非空值;
List<? super Integer> 接受 Integer 值,但只能产生 Object。
“只读”和“只写”只是简写,并非能力规则:两种列表仍可被查询、清空,
或者在实现允许时接收 null。
捕获和推断 #
每个通配符都表示一个未知的捕获类型。通配符容器无法表达两个独立通配符值具有相同类型, 因此在关系重要时使用泛型辅助方法:
static void reverse(List<?> list) { reverseCaptured(list); }
private static <T> void reverseCaptured(List<T> list) {
Collections.reverse(list);
}方法推断会结合参数约束、目标类型和声明的边界。菱形运算符会为构造器调用推断类型参数;
但它不会使原始类型变得安全。边界可以要求特定能力:
<T extends Comparable<? super T>> T max(List<? extends T> values)。
交集边界可以包含一个类,后面跟随多个接口。
擦除下的构造 #
new T()、new T[n] 和 T.class 都是非法的,因为擦除不会保留运行时所需的构造器、
组件类型或类字面量。应改为传入工厂:
static <T> T create(Supplier<? extends T> factory) {
return factory.get();
}对于反射集成,接受 Class<T> 并处理其受检查的反射异常;但如果由调用方负责构造,
优先使用 Supplier<T>。泛型数组仍受到限制,因为数组是具体化且协变的,而泛型类型会被擦除且具有不变性。
评论
在 GitHub 上阅读或评论(发表评论需要登录)