泛型拾遗
基本设计原则
- how codes should vary in different types.
- compatible with other release.
- before generic class,generic programming was achieved with inheritance:在所有后来使用T的地方使用Object或者Object数组。经典的猫狗列表问题的来源。
基本语法
- java 的泛型没有 template 关键字。表面上(superficially)看和 C++ 并无二致,实际上有大量差别
- 三种基本概念:
- Type Parameter(类型形参):在泛型类、接口或方法声明时使用的标识符。
class Box<T>
中的 T。- 但是在 Java 核心技术中,
List<String>
中的String也经常被描述为 Type Parameter。Container.class.getTypeParameters()
得到的也是T
。
- Type Variable(类型变量):类型参数在代码中的引用。
- 可以认为是类型参数的一个”实例”。在
class Box<T> { T value; }
中,value 的类型T是一个类型变量(而不是占位符T)。许多文档中”类型参数”和”类型变量”可互换使用。
- 可以认为是类型参数的一个”实例”。在
- Type Argument(类型实参):在使用泛型类型时,提供的具体类型。
- 在调用或实例化时提供。
Box<Integer>
中的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(
- 在调用或实例化时提供。
- 在 C++ 里,
f(g<a,b>(c))
有两个意思:使用g<a,b>(c)
的结果调用f;或者使用g<a
和b>(c)
调用 f。所以 java 泛型参数要写在方法前面,类型见证也在方法前面。List<T>
是 generic type。List<String>
是 Parameterized type。
- Type Parameter(类型形参):在泛型类、接口或方法声明时使用的标识符。
边界/绑定类型(bounding type)
在Java泛型的上下文中,”bound”一词最准确的翻译是“边界”(boundary)而非”绑定”。Java官方文档一致使用”upper bound”(上边界)和”lower bound”(下边界)这样的术语,这些术语在数学和集合论中表示边界概念。
- 在类型参数 T 声明时就指定 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 {}
- 确保类型安全:
<E extends Enum<E>>
这种递归泛型定义确保了枚举类型只能被其子类实例化 - 提供类型正确的方法:使得像 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-易被忽略
- 各种构建器模式实现
注意和 Comparable 比较
<T extends Comparable<T>>
意味着 T 加入了一种以自己为目标的混型能力。RepresentationModel<T extends RepresentationModel<? extends T>>
意味着这种类型要使用某些父子类型都共用的操作,必须让操作能够返回子类型而不是父类型。实现自引用类型层次结构。- 类似的例子还有:
public static <T> List<T> unmodifiableList(List<? extends T> list)
。这种 T 总是在远处作为一个起点使用。实际的类型的多重绑定还要出现在方法参数或者返回值里。
- 类似的例子还有:
Comparable<T extends Comparable<? super T>>
,意味着需要与父类型进行比较操作。
类型擦除(type erasure)
- 擦除的目的:为了 interoperability between generic and legacy code。所以把类型信息全擦除成无泛型,能在 java 1.0 上运行的字节码。
- 类型擦除的结果:将 type variable 替换成绑定类型(BoundingType)而不是类型实参的 ordinary class。如果
B<T extends Comparable>
而A extends B<C>
。那么A编译过后,得到的是一个B内所有T都替换成Comparable
的类,但是在写代码的时候,编译器会在赋值的时候检查,且在取值的时候转型,保证所有的T相关的方法的类型全部都是C而不是 Comparable。如果我们声明的是B<T>
,我们会得到使用 raw type 的 class(如ArrayList
之于ArrayList<String>
)-在字节码层面T全部换成了Object
,即使我们在IDE里感觉我们仍然在使用C
这一类型。 - 有了类型擦除,所有的泛型表达式都是可以被翻译的。带有泛型的代码被编译器翻译后通常分为两个部分:1. ordinary class,带有 bounding type,2. casting code,带有类型转换的代码。
- cast code 是为了保证类型安全而存在的。这些代码有些(普通的泛型set)是在编译时通过检查实现的,有些则是在代码里加上一些字节码指令实现(get和桥接的set)的。
- 假设
B extends A<C>
,我们调用calc(new C())
的时候,父类型的相关方法总是会被擦除到 bounding type(假定 C 的擦除类型是 Object),所以子类的方法也带有这个 bounding type 的方法实现calc(Object object)
。但为了保持多态,如果子类也实现了一个calc(C c)
,编译器就会生成了一个calc(Object c)
去调用calc(C c)
(相当于把重载桥接成多态,见下面的“擦除的实际工作流程”),真正要使用多态,就必须产生一个 synthesized bridge method,执行calc(Object object) {calc((C)object);}
。这个东西是为了保持多态(preserve polymorphism)。 - 为了兼容性(compatibility)考虑,
A a = new A<String>()
之类的赋值(从泛型到擦除类型的赋值总是会成立的)总是会成立,但编译器总是会报 warning(作为原始类型 'generic. Pair' 的成员对 'setFirst(T)' 的未检查的调用
)。在 Jackson 中经常遇见如下问题:实际反序列化的生成 class 是 LinkedHashMap,但Entity<A<B>> = JsonUtil.toObject(str)
等还是会赋值成功(此处 实际得到的是A<LinkedHashMap>
)。
擦除的实际工作流程
假设我们有一个 Pair 类:
1 |
|
我们产生子类如下
1 |
|
按照常见的规则,编译器会把所有的方法签名都改掉,如setSecond(T newValue)
会被先编译为setSecond(LocalDate newValue)
。
但是如果我们使用反编译器去尝试反编译这个类javap -c DateInterval.class
,我们会发现如下事实:
1 |
|
并不存在一个被重载的方法,什么换签名方法都不存在!
而如果我们这样写:
1 |
|
我们会一次在子类里得到2个setSecond方法:
1 |
|
public void setSecond(java.time.LocalDate)
是我们定义的显式方法,而public void setSecond(java.lang.Object);
是编译器帮我们合成(synthesized)的桥接方法(bridige method)。
1 |
|
所以总结起来:
Pair<T>
本质上 BoundingType 是 Object。extends Pair<LocalDate>
得到的是一个所有 T 都 substitute 成 Object 的类型,作为运行时的无类型化类型使用。- 但是编译器会在编译 set 方法的时候严格限制类型,如果使用了非 LocalDate 的类型设值必定发生编译错误,而如果使用 get 方法,取出来的值会隐藏一个 checkcast 的字节码指令。
- 在调用子类实例的方法之前,会产生一个擦除类型的方法,即
setSecond(java.lang.Object)
,在内部会有一个 checkcast 指令的实现,达到setScond((LocalDate)second)
的效果。 pair.setSecond(LocalDate.now());
的时候,指向的是父类Pair<LocalDate>
而不是DateInterval
,调用setSecond只能指向父类方法,而不能是子类方法。但实际上我们调用的首先是桥接方法,然后才到具体类型的方法(栈帧为证)。也就是说,编译器知道推测方法的调用顺序,以及我们在试图override我们没有override的方法,因为父方法的签名是setSecond(java.lang.Object)
,我们声明一个setScond((LocalDate)second)
实际上是在overload这个方法。编译器帮我们把overload转成了override,我们即使加上@override
注解也没问题,而且调用的时候能够实现多态。
所谓的泛型确实只有编译器才存在,参数实例化的泛型类里只留存一些绑定类型的边界信息,无边界泛型类的父方法的类型信息都是 Object,类型安全都是在编译器通过1检查、2插入桥接方法、3取值的时候cast实现的。这种实现保证了运行时无类型,又确保程序有多态效应。
验证桥接方法有2种方法:
- 反射检查:使用Method.isBridge()判断方法是否为桥接方法。
Method method = DateInterval.class.getMethod("setSecond", Object.class);
- 字节码查看:通过
javap -c DateInterval.class
查看生成的桥接方法,其修饰符包含ACC_BRIDGE(本例中没有试出来)。如果直接查看 Pair.class,我们会看到方法仍然使用泛型:public void setSecond(T);
。桥接方法仅在子类主动重写父类泛型方法时生成。
桥接方法的另一种例子
一个类型实现了 Clonable,天然是 Object 的子类。它实现自己的clone的时候,会得到两个方法:
1 |
|
类型 clash
1 |
|
两个method本身不是重载,直接签名冲突:'method(T)' 与 'method(Object)' 冲突;两个方法具有相同的擦除
。
与遗留代码的冲突
把 generic class 的实例赋给 raw type class 的实例会遇到编译器警告;反过来,把 raw type class 的实例赋给 generic class 的实例也会遇到编译器警告-在跨模块传递数据时,后者往往更危险。
现代代码编写原则是:
- 原理早期的数据结构。
- 全程使用 generic class 而不是 raw type class-除非你使用反射或者反序列化不得不得到 raw type,你必须十分清楚内存的实际类型,然后
@SuppressWarnings("unchecked")
。 - 我们可能可以把
List<Integer>
赋给List<String>
,这会导致堆污染(heap pollution)。
Type Witness 类型见证/目击者
1 |
|
其中第二种情况更常见,如果方法调用/=左边有足够的目标类型,编译器不会产生有歧义的推导(Inference)路径。
泛型不能做什么(Restricstions and Limitations)
所有泛型的 bug 都与一个基本假设有关,在虚拟机里运行的类型实际上使用的是 raw type,不注意这一点就可能出错。parameterized type vs raw type。
不能使用基本类型实例化 type parameters
C++可以。
动态类型识别(inquiry)只能在 raw type 上工作
instanceof、Class ==、getClass() 这类操作都只能当做 raw type 工作:
- 实际上不能
a instanceof Pair<String>
,这会导致一个编译错误 - 可以
(Pair<String>)a
,这会导致一个编译警告。 stringPair.getClass() == employeePair.getClass() // true
不能创建 ParameterizedType 作元素的数组
但可以声明和赋值到泛型数组,这就是 GenericArrayType 的用处。
- GenericArrayType 是
T[]
或List<String>[]
,List<String>
是ParameterizedType。 - -创建不可能:
Pair<Sring>[] a = new Pair<String>[10]
是不可能的。是Pair<String>
而不是 T。T[] arr = new T[10];
是不可能的。var table = (Pair<String>[])new Pair<?>[10]
是合法的,可以 new 通配符数组,然后强转型。
Object[] arr = new Pair<String>[10]
这样是合法的,而且这个arr实际上是Pair[]
,所以它可以接受任意的Pair
-不管是Pair<String>
还是Pair<Employee>
,但不能接受String
。这会导致 cast 异常。
类型参数可以和vargargs
一起工作
如果使用@safevarargs
连警告都不会有。也就是说我们可以这样创建泛型数组Pair<String>[] pair = makePairs(pair1, pair2)// makePairs 的签名是(T... arr),且直接返回这个 arr
(这一条破坏了“不能创建parameterized type 的数组”规则)。
但泛型数组还是危险的,因为数组本身还是 raw type,往里面写入一个Pair<Integer>
可以,取出来会有转型问题。
不能初始化 type variables:
- 最常见的非法写法是
new T()
。- 因为 T 实际上会擦除到 Object,不能调用程序员意图里的那个构造器。
- 但 T t 是可以且很常见的。
- workaround:
- java8:
Supplier<T> constr constr.get()
,然后在使用的时候,构造器的方法引用就是一个Supplier<T>
。 - java8以前:在使用 T 的地方都使用
Class<T>
,然后借反射来生成对应的对象:Class<T> cl cl.getConstructor().newInstance()
。
- java8:
不能实例化泛型数组
T[] a = new T[1] // error
如果我们需要得到某种类型的数组,可以采取如下方法:
- 先找到一个泛型 List,然后使用下文提到的“空泛型数组的拷贝方法”。
- 想办法生成一个数组,然后
(T[])
做转型-实例化不可以,但是转型可以。
这一条和上面的 ParameterizedType 条目差不多。
泛型类的 type variables 不能在它的 static 上下文里工作
但静态方法自己可以有 type variables。
我们可以声明一个class Single<T>
,但这个Single
里的静态方法不能引用T
。静态方法里可以自己声明另一个<U>
。
不能抛出和捕获泛型类
T extends Throwable
合法。method throws T
合法,throw (T)t
合法。catch(T t)
非法。Problem<T> extends Exception
非法。
巧妙地使用泛型,可以破坏 checked exception 的限制
1 |
|
我们骗过了编译器,让它认为asRunnable
抛出的不是受检异常:
- 调用 asRunnable 以后,我们不需要写受检异常的 handler,也不需要
new RuntimeException(ex)
来 wrap。 - 运行时
throw (T) t;
不会报错,因为擦除是擦除到 Throwable,我们仍然在更上游遇到这个异常时和遇到普通异常的流程是一致的。
永远不要写boolean equals(T object)
因为每个泛型方法都有一个兜底的 raw type 方法兜底,如果兜底方法和父类的非泛型方法相冲突(clash),编译会报错。
编译器会擦除出 boolean equals(Object object)
,制造同签名的方法,连重载都算不上。
只能写:
1 |
|
这样构成了显式override。
所以这里有一个极端重要的认知:
- 我们不能在泛型类里声明擦除后有同签名的方法。
foo(T o)
和foo(Object o)
不算一种重载,算一种 clash。但我们可以写@override foo(Object o){}
来 override。 - 我们可以在实例化的子类里面写具体类型来 overrload 擦除方法,编译器会帮我们把overload转成 override。例子见上面“擦除的实际工作流程”。
再谈数组和泛型类的继承和协变
数组隐藏有类型信息。所以虽然数组是协变的,但是
1 |
|
但是泛型类是完全擦除的,所以对于 A List 里插入 B 元素,如果允许继承,则真的会发生混合插入的问题。
Java 有一个习惯,就是不完全防止堆污染,有时候靠在取值的时候抛出异常来解决。
泛型能够做什么
- type variable 可以拿来做目标参数,如
T t = get()
、(T)
,(T[])
。 - 我们始终可以把 parameterized type 往 raw type 转。
ArrayList<Integer>
可以往List<Integer>
转-我们经常是这样声明的。
通配符(wildcard)
- ? 表明了 Type variable 是怎样 vary的。
- 我们已经知道泛型不是协变的,但是 ? 用来实现某些人类似协变的需求,产生了一个泛型类家族的父类/子类(通配符允许泛型类的继承树在泛型层面出现,而普通泛型则只允许泛型类的继承树在外部类的层面出现)。这增加代码的灵活性,同时保持类型安全。
1 |
|
反直觉的两种子类关系,子类意味着类型兼容:
Pair<? extends Employee>
意味着它的子类 可以是Pair<Employee>
和Pair<Manger>
。Pair<? super Employee>
意味着它的子类(而不是超类)可以是Pair<Employee>
和Pair<Object>
。
PECS 原则
编译器在编译的并不会真的把 ? 换成具体类型-虽然我们经常在概念上这样想。
PECS 讨论的是,Employee 作为目标类型,什么时候只能消费-作为参数/右值,什么时候只能生产-作为返回值/左值。
? extends T
作为一个 type parameter 证明可以在 T t 处读。? super T
作为一个 type parameter 证明可以在 T t 处写。
这个原则很好地示范了定义(简单性)-例子(复杂性)-助记符(简单性)之间的关系。
1 |
|
编译器不知道,所以干脆不允许 set/call(消费),但可以get。
1 |
|
用赋值和设值,用 collection 的 get 和 add 比较好理解这个规则。
日期的例子-?和 T 同时出现
LocalDate
是ChronoLocalDate
的子类(顺序就是这样,没有反过来),但 ChronoLocalDate 已然实现了 comparable<ChronoLocalDate>
。这时候 LocalDate 的比较方法就应该声明成<T extends Comparable<? super T>>
。这是减轻 caller 程序员负担,而把复杂度留给库本身的一种做法。
还有一个例子:int compareTo(? super T)
,这意味着通配符不是T的实参。
从 A 派生出 B,则 B 的 comparable 方法必须声明为可以支持 super,这样对 A 的 compare 才能同时兼容 A、B - 而不只是 B,Lists 的 removeIf 方法的谓词同理。
举例,Java 8 中的 ArrayList 有个 removeIf 的方法,它的参数是个 predicate,但这个 predicate 的实参可以是,比如 Employee,也可以是 Object(用上了 super):default boolean removeIf(Predicate<? super E> filter)
。
更严格的<?>
?在没有其他类型 extends/super 的时候,只能在 get 赋值用,等同于<? exntends Object>
。再次强调,?不是T的实参。
Pair<?>
是个没用窝囊(wimpy)的类型,它的setFirst(? object)
方法甚至无法被使用-试想,根据 PECS 法则,setFirst
怎样确定它的设值是兼容某个类型的?而getFirst
方法也只能赋给 Object。
但 Pair 的 set 方法可以使用任意类型,而 get 方法可以赋给 Object。
但如果有些场景只是从某类Pair<T>
内部读值,那么Pair<?>
比Pair<T>
更加简洁易读。
通配符捕获:单层<?>
参数方法套着单层<T>
参数方法,而逻辑是在内层方法里写的
? 不是 type variable,是 wildcard variable。
通配符捕获要解决的问题是:我们无法用 ? 来写代码,不能当作左值的类型来用。? a = Pair.getFirst()
是不合法的。所以我们无法写 swap 方法:
通配符捕获的解法是:引入一个 T 来捕获通配符,用 T 来写代码:
1 |
|
在这里,T 捕获了通配符。T 不知道 ? 的具体类型,但知道它是某个确定类型-就当 Object 用。
编译器只有在确定 T 可以捕获确定的通配符的时候才允许编译通过。例如List<Pair<?>>
多了一个间接层,一个 list 可能有不同的 Pair<?>
(这足以证明?即使是在一个数据结构里,也可能产生多个类型Pair1<?>
内部的的?是同一种?,但是 Pair2<?>
完全是另一种),持有不同的具体类型,编译器不会允许捕获 List<Pair<T>>
里的 T。
1 |
|
需要的类型: List <List<T>> 提供的类型: List <List<?>> 原因: 不兼容的相等约束: T 和 ?
幽灵类型
例子来自于《Java中的幽灵类型》和《你真的懂 Builder 设计模式吗?论如何实现真正安全的 Builder 模式》。
忽略了特别复杂的阶段式构造,因为它是与泛型无关的。
构造类型的用法
- 用接口子类当作状态,而不是枚举,这样可以作为 generic 的bound使用。
- 定义泛型类,泛型类只有私有构造器,不允许外部创建,因为这是泛型类,所以构造器在调用的时候是一定可以带有类型见证的,如
new Plane<Landed>(10)
。 - 定义初始构造方法,只允许生成初始态实例。
- 关键:定义复制构造方法,允许
? extend FlightStatus
这个类型族的实例相互拷贝,这样产生了外层方法。 - 定义各种跳转方法, 关键:在签名和返回值上做幽灵类型的切换,内部只使用同一个参数族方法复制来复制去,每次复制的时候使用新的类型见证
return new Plane<Flying>(p);
。
不基于接口的用法
1 |
|
builder pattern
1 |
|
- 把 builder 泛型化,每个字段对应一个泛型参数。
- 定义 TRUE、FALSE 两个状态。
- 构造器要求字段对应的泛型参数全部都是 TRUE。
- 每个 builder 只保留一个本参数对应的类型实参,其他部分仍然泛化-部分泛型。
- 如果一个字段有默认值,它不具有对应的泛型参数。
泛型与反射
泛型与 class
String.class
是Class<String>
的一个 object。
Class 的 type variable 实际上限制了它方法的种种返回值。
Java 在5以后,确切地把各种constructor相关的方法都<T>
参数化了。
反射能够在擦除后知道些什么
可以知道的以下东西:
- 一个方法或者类型有个 type parameter T。
- T 有 super 或者 extends 的 bound。
- T 有个 wildcard variable - wildcard 和 T 会同时存在。
不可以知道的东西
- 到底运行时绑定的 type parameter 是什么?how type parameter were resolved.
获取类型信息
Type[] actualTypes = ((ParameterizedType) type).getActualTypeArguments()
什么时候能够得到String
?
丢失类型信息就无法 ParameterizedType 转型。
什么情况下会丢失类型信息
- 方法参数(如
void process(List<T> list)
)。 - 局部变量(如
List<T> list = new ArrayList<>()
)。 - 泛型方法的类型推断(如
Collections.emptyList()
)。
什么情况下能够获取到类型信息
- 类或接口继承时显式指定泛型参数(如 class MyList extends ArrayList
)。 - 使用匿名内部类:
List<String> list = new ArrayList<String>() {}; // 匿名子类
- gson的类型令牌:
List<String> list = gson.fromJson(json, new TypeToken<List<String>>() {}.getType());// 这里获得的 Type 是最顶层的java Type 接口
- gson的类型令牌:
1 |
|
反射的类型 hiarachy
反射的基础类型是Type
接口,它有五个子类:
Class
描述具体类型(而不是接口)。我们常见的类型如果本身不带有<>
就属于这一类型。对 Java 1.5 以后的泛型,一个Map.class
和Map<String, Integer>
的 class 在求等上完全相等,但唯有引入其他 Type 子类才能说明String
和Integer
的存在。
- 最常见的 Type 实现类。
- 代表原始类型(raw types)和基本类型(primitive types)
1 |
|
TypeVariable(类型变量)
- 表示泛型中的类型变量
- 例如泛型定义中的 T、K、V 等
1 |
|
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)。 - 主要方法:
getActualTypeArguments()
: 获取泛型参数的实际类型getRawType()
: 获取原始类型getOwnerType()
: 获取所属类型
1 |
|
GenericArrayType(泛型数组类型)
GenericArray 接口,描述泛型数组如 T[]
。这可以被用在获取方法参数上。获取到具体参数以后,可以把它转化为 ParameterizedType,然后获取它携带的 actualParamters。例如:T[]
或List<String>[]
1 |
|
WildcardType(通配符类型)
表示通配符泛型,如 ?
、? extends Number
、? super Integer
。它必然可以找到 up bound(最起码有 Object),不一定有 lower bound。
1 |
|
主要使用场景
- 反射获取泛型信息
- 泛型类型系统的实现
- 框架开发中的类型判断和处理
- 注解处理器的开发
- 序列化/反序列化框架
这些类型系统的实现让 Java 能够在运行时获取和处理泛型信息,尽管 Java 的泛型是通过类型擦除实现的,但通过这些接口我们仍然可以在运行时获取到泛型的类型信息。
TypeLiteral
有一个可以捕获多重泛型的实参的方案。
1 |
|
运行时捕获类型参数的方法
方法一
1 |
|
方法二
方法三
1 |
|
这个方法来自于 JDK 5 的作者的博客《super-type-tokens》。
- abstract class 保证了这个类型必须通过子类确定,这样
getGenericSuperclass
必定会得到一个 ParameterizedType 而不仅仅是一个 GenericType。 implements Comparable<TypeReference<T>>
并不是真的希望子类覆写一个比较方法,而是希望子类型不要实现成一个 raw type。
保证了这两天_type
一定是一个 concret class。
各种泛型黑魔法
获取通配符的 bound
这里不是编程时拿?来用,而是确切知道?的 bound 是什么:
1 |
|
Optional 里通配符转成类型参数
可见@SuppressWarnings("unchecked")
java 的基础库里到处都是。
1 |
|
确认消费类型的两种方法
1 |
|
空泛型数组的拷贝方法
1 |
|
这个方法的特点:
- 自动调整数组大小
- 可以像 go 一样往参数填充数据
- 类型安全:
String[] rightWay = list.toArray(new String[0]);
优于String[] wrongWay = (String[]) list.toArray();