泛型拾遗
类型系统
Class
- 最常见的 Type 实现类
- 代表原始类型(raw types)和基本类型(primitive types):
1 |
|
ParameterizedType(参数化类型)
- 表示带有泛型参数的类型
- 主要方法:
- getActualTypeArguments(): 获取泛型参数的实际类型
- getRawType(): 获取原始类型
- getOwnerType(): 获取所属类型
1 |
|
TypeVariable(类型变量)
- 表示泛型中的类型变量
- 例如泛型定义中的 T、K、V 等
1 |
|
GenericArrayType(泛型数组类型)
- 表示元素类型是参数化类型或类型变量的数组
- 例如:T[] 或 List
[]
1 |
|
WildcardType(通配符类型)
- 表示通配符泛型,比如 ?、? extends Number、? super Integer
1 |
|
主要使用场景:
- 反射获取泛型信息
- 泛型类型系统的实现
- 框架开发中的类型判断和处理
- 注解处理器的开发
- 序列化/反序列化框架
这些类型系统的实现让 Java 能够在运行时获取和处理泛型信息,尽管 Java 的泛型是通过类型擦除实现的,但通过这些接口我们仍然可以在运行时获取到泛型的类型信息。
基本语法
- java 的泛型没有 template 关键字。表面上(superficially)看和 C++ 并无二致,实际上有大量差别
- 三种基本概念:
- Type Parameter(类型形参):在泛型类、接口或方法声明时使用的标识符。
class Box<T>
中的 T。- 但是在 Java 核心技术中,
List<String>
中的 String 也经常被描述为 Type Parameter。
- Type Variable(类型变量):类型参数在代码中的引用。
- 可以认为是类型参数的一个”实例”。在
class Box<T> { T value; }
中,字段类型T是一个类型变量。许多文档中”类型参数”和”类型变量”可互换使用。
- 可以认为是类型参数的一个”实例”。在
- Type Argument(类型实参):在使用泛型类型时,提供的具体类型。
- 在调用或实例化时提供。Box
中的 Integer。 1
2
3
4
5
6
7
8
9
10
11
12
13
14// 类型参数T在类声明中引入
public class List<T> {
// T用作类型变量
private T[] elements;
// T继续用作类型变量
public void add(T element) {...}
// E是新的类型参数,用于此方法
public <E> void addAll(Collection<E> items, Function<E, T> converter) {...}
}
// String是类型实参,实例化时提供
List<String> names = new ArrayList<>();- 泛型类的 type parameter 的声明在类背后。泛型方法的 type parameter 在 modifier(
public static
)和return value
之间。
- 泛型类的 type parameter 的声明在类背后。泛型方法的 type parameter 在 modifier(
- 在调用或实例化时提供。Box
- 在 C++ 里,
f(g<a,b>(c))
有两个意思:使用g<a,b>(c)
的结果调用f;或者使用g<a
和b>(c)
调用 f。所以泛型参数要写在方法前面。List<T>
是 generic type。List<String>
是 Parameterized type。
- Type Parameter(类型形参):在泛型类、接口或方法声明时使用的标识符。
边界/绑定类型(bounding type)
在Java泛型的上下文中,”bound”一词最准确的翻译是“边界”(boundary)而非”绑定”。Java官方文档一致使用”upper bound”(上边界)和”lower bound”(下边界)这样的术语,这些术语在数学和集合论中表示边界概念。
- 明确要绑定 type variable 到某个类型(
T extends BoundinType
,擦除到这个 BoundinType)-得到 type argument,才可以在接下来的方法里调用那个类型的方法,如需要compareTo
方法,就必须<T extends Comparable<T>>
。extends
本身可以后接多个绑定类型,用&
分隔。- 这点同 C++ 是一样的,我们是为了调用某个方法才指定了 bound。
- extends 代表 subtype concept 的 approximation,并不是扩展的意思,在 bound 的使用里,它统一了扩展与实现。
- extends 只能有一个 class,用ampersand(
&
)来分隔各种类型,用,
来分隔 type variables。这会制造有限的菱形继承问题-这和 java 单继承的思路一致。T extends Comaprable & Serializable
会把 T 擦除到 Comaprable(first bound),在某些需要作为 Serializable 的时候则编译器插入一个 cast instruction(如果必要的话,java 甚至会对.
取值操作符进行 cast指令插入) - java 的编译器做这种插入对程序的其他部分算得上是可插拔的了。
- extends 只能有一个 class,用ampersand(
递归泛型边界模式(Recursive Generic Type Bounds)
这是被称作”F-bounded多态”(F-bounded polymorphism)或”递归泛型边界”的高级泛型设计模式。
1 |
|
- T必须是RepresentationModel的子类
- 并且这个子类的泛型参数必须是T自身或T的某个子类
在这个例子里:RepresentationModel - type parameter 是 -> T <- 是子类,且这个子类的 type argument 是 T 自己 - RepresentationModel
。
如果不使用这种类型:
1 |
|
那么使用时会遭遇编译错误:
1 |
|
这个问题的核心是,有些 fluent API 本身返回自身,这种自身不能返回子类时,父子混合 build 会产生奇怪的问题。
解法:
1 |
|
使用时:
1 |
|
这种方案的做法是:
- 父类所有方法都使用 T 作为 type variable。在父转子的时候,使用转型,忽略警告。
- 流式调用的时候都转为自己。
核心优势:
- 类型安全的方法链:子类调用父类方法时返回的仍然是子类类型。
- 代码复用:父类可以实现通用功能,子类无需重写这些方法。
- IDE支持:智能提示和自动完成可以正确显示返回对象的可用方法。
这种模式广泛应用于各种Java库中:
- Java的
Enum<E extends Enum<E>>
。public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable {}
- 确保类型安全:
> 这种递归泛型定义确保了枚举类型只能被其子类实例化 - 提供类型正确的方法:使得像 compareTo 这样的方法能够正确接收子类类型的参数
- 实现自引用泛型:允许枚举类引用自身类型
- 极端重要!更复杂的例子,
<
前边的部分,恰好是>
前边的部分加上一个<?>
,中间插入一个? extends
:java.lang.Object&java.lang.Serializable&java.lang.Comparable<? extends java.lang.Object&java.lang.Serializable&java.lang.Comparable<?>>
- 确保类型安全:
- JPA的CriteriaBuilder
- Stream API-易被忽略
- 各种构建器模式实现
类型擦除(type erasure)
- 类型擦除的结果的是将 type variable 替换成绑定类型(bounding types)的 ordinary class(或者说使用 raw type 的 class)。
- 有了类型擦除,所有的泛型表达式都是可以被翻译的。带有泛型的代码被编译器翻译后通常分为两个部分:1. ordinary class,带有 bounding type,2. casting code,带有类型转换的代码。
- cast code 是为了保证类型安全而存在的。
- 假设
B extends A<C>
,我们调用calc(new C())
的时候,父类型的相关方法总是会被擦除到 bounding type(假定 C 的擦除类型是 Object),所以子类的方法也带有这个 bounding type 的方法实现calc(Object object)
,但为了保持多态,编译器又生成了一个calc(C c)
(相当于做了一次重载),真正要使用多态,就必须产生一个 synthesized bridge method,执行calc(Object object) {calc((C)object);}
。这个东西是为了保持多态(preserve polymorphism)。泛型集成对比点 1。 - 为了兼容性(compatibility)考虑,
A a = new A<String>()
之类的赋值(从泛型到擦除类型的赋值总是会成立的)总是会成立,但编译器总是会报 warning。猫插入狗列表中问题,只有在真实地 set 和 get 操作时才会发生。在 Jackson 中经常遇见如下问题:实际反序列化的生成 class 是 LinkedHashMap,但Entity<A<B>> = JsonUtil.toObject(str)
等还是会赋值成功(此处 实际得到的是A<LinkedHashMap>
)。
Type Witness 类型见证/目击者
1 |
|
其中第二种情况更常见,如果方法调用/=左边有足够的目标类型,编译器不会产生有歧义的推导(Inference)路径。
泛型不能做什么(Restricstions and Limitations)
所有泛型的 bug 都与一个基本假设有关,在虚拟机里运行的类型实际上使用的是 raw type,不注意这一点就可能出错。parameterized type vs raw type。
- 不能使用基本类型实例化 type parameters。
- 动态类型识别只能在 raw type 上工作,也就是 instanceof、Class ==、getClass() 这类操作都只能当做 raw type (
instanceof ArrayList<String>
和instanceof ArrayList<Integer>
是一样的)工作。 - 不能创建 parameterized type 的数组(
Pair<Sring>[] a = new Pair<String>[1]
是不可能的) - 但可以声明和赋值到泛型数组,这就是 GenericArrayType 的用处。
- 类型参数可以和 vargargs 一起工作,如果使用
@safevarargs
连警告都不会有。也就是说这样的语句是成立的Pair<String>[] pair = makePairs(pair1, pair2)
(这一条破坏了第三条规则)。但泛型数组还是危险的,因为它是协变(covariant)的。泛型数组适合被写,但不适合被读。尽量避免使用它,否则会出现很奇怪的运行时错误。 - 不能初始化 type variables,最常见的非法写法是
new T()
(但 T t 是很常见的)。一个 workaround 是在使用 T 的地方都使用Class<T>
,然后借反射来生成对应的对象。 - 不能创建泛型数组(
T[] a = new T[1]
)。 - 泛型类的 type variables 不能在它的 static 上下文里工作。但静态方法自己可以有 type variables。各有各的泛型。
- 不能抛出和捕获异常类。
- 巧妙地使用泛型,可以破坏 checked exception 的限制
- 因为每个泛型方法都有一个兜底的 raw type 方法兜底,如果兜底方法和父类的非泛型方法相冲突(clash),编译会报错。举例,永远不要写
boolean equals(T object)
,因为编译器会擦除出boolean equals(Object object)
,制造同签名的方法,连重载都算不上。
泛型能够做什么
- type variable 可以拿来做目标参数,如
T t = get()
和(T)
。
通配符(wildcard)
?是通配符。
? extends T 作为一个 type parameter 证明可以在此处读。Pair<? extends Employee>
意味着它的子类可以是Pair<Employee>
和Pair<Manager>
。
? super T 作为一个 type parameter 证明可以在此处写。Pair<? super Employee>
意味着它的子类 可以是Pair<Employee>
和Pair<Object>
。
LocalDate
是ChronoLocalDate
的子类(顺序就是这样,没有反过来),但 ChronoLocalDate 已然实现了 comparable<ChronoLocalDate>
。这时候 LocalDate 的比较方法就应该声明成<T extends Comparable<? super T>>
- 这是 comparable 的标准泛型方案。从 A 派生出 B,则 B 的 comparable 方法必须声明为可以支持 super 的类型,这样对 A 的 compare 才能同时兼容 A、B - 而不只是 B,Lists 的 removeIf 方法的谓词同理。(泛型集成对比点 2)
通配符的存在实际上是为了放松“泛型不能支持协变”,而需要让程序员灵活使用多种实际类型做的一个妥协。
举例,Java 8 中的 ArrayList 有个 removeIf 的方法,它的参数是个 predicate,但这个 predicate 的实参可以是,比如 Employee,也可以是 Object(用上了 super)。
Pair<?>
是个没用窝囊(wimpy)的类型,它的setFirst(? object)
方法甚至无法被使用(试想,setFirst 怎样确定它的设值是兼容某个类型的?,?实际上近于 ? extends)。 但如果有些场景只是从某类Pair<T>
内部读值,那么Pair<?>
比Pair<T>
更加简洁易读。
通配符捕获
? 不是 type variable,是 wildcard variable。
? a = Pair.getFirst()
是不合法的。
引入一个 T 来捕获通配符,就可以执行这个方法:
1 |
|
在这里,T 捕获了通配符。T 不知道 ? 的具体类型,但知道它是某个确定类型。
编译器只有在确定 T 可以捕获确定的通配符的时候才允许编译通过。例如List<Pair<?>>
多了一个间接层,一个 list 可能有不同的 pair,持有不同的具体类型,编译器不会允许 List<Pair<T>>
产生捕获。
另一个例子:
1 |
|
泛型与反射
泛型与 class
String.class 是Class<String>
的一个 object。
Class 的 type variable 实际上限制了它方法的种种返回值。
反射能够在擦除后知道些什么
可以知道的以下东西:
- 一个方法或者类型有个 type parameter T。
- T 有 super 或者 extends 的 bound。
- T 有个 wildcard variable。
不可以知道的东西
- 到底运行时绑定的 type parameter 是什么?
反射的类型 hiarachy
反射的基础类型是 Type 接口,它有五个子类:
- Class 类,描述具体类型(而不是接口)。我们常见的类型如果本身不带有
<>
就属于这一类型。对 Java 1.5 以后的泛型,一个 Map.class 和Map<String, Integer>
的 class 在求等上完全相等,但唯有引入其他 Type 子类才能说明 String 和 Integer 的存在。 - TypeVariable 接口,描述类型参数,如 T
- WildcardType 接口,描述通配符,如 ?。它必然可以找到 up bound(最起码有 Object),不一定有 down bound。
- ParameterizedType 接口,描述泛型类或接口类型,如
Comparable<? super T>
- 奇怪,是 T 而不是 String。常见的 ParameterizedType 有 Collection 的 Class 实例。可以简单把 ParameterizedType 理解为 指向 Collection 类的特殊的 class,它携带的 actualParamters 就是String/Integer 的 class,而 WildcardType 的 bounds 就是 extends A 的那个 A 的class。它还有一个 getRawType(也就是 Map),和 ownerType(也就是 Map 之于 Entry)。 - GenericArray 接口,描述泛型数组如 T[]。这可以被用在获取方法参数上。获取到具体参数以后,可以把它转化为 ParameterizedType,然后获取它携带的 actualParamters。
TypeLiteral
有一个可以捕获多重泛型的实参的方案。
1 |
|
运行时捕获类型参数的方法
方法一
1 |
|
方法二
方法三
1 |
|
这个方法来自于 JDK 5 的作者的博客《super-type-tokens》。
- abstract class 保证了这个类型必须通过子类确定,这样 getGenericSuperclass 必定会得到一个 ParameterizedType 而不仅仅是一个 GenericType。
implements Comparable<TypeReference<T>>
并不是真的希望子类覆写一个比较方法,而是希望子类型不要实现成一个 raw type。
保证了这两天_type
一定是一个 concret class。
各种泛型黑魔法
Optional 里通配符转成类型参数
可见@SuppressWarnings("unchecked")
java 的基础库里到处都是。1
2
3
4
5
6
7
8
9
10/**
* Common instance for {@code empty()}.
*/
private static final Optional<?> EMPTY = new Optional<>();
public static<T> Optional<T> empty() {
@SuppressWarnings("unchecked")
Optional<T> t = (Optional<T>) EMPTY;
return t;
}
确认消费类型的两种方法
1 |
|
空泛型数组的拷贝方法
1 |
|