Java泛型
Java泛型
1. 泛型的基本定义
Java 中的泛型是 伪泛型,这是因为 Java 中的泛型会在编译期被擦除、在编译后通过强转实现类型的约束,换言之一个类不论具有多少种泛型变体,其编译后的类型都指向同一个原始类的字节码,好处是避免编译产物膨胀。而相比之下 C++ 的泛型(实际上是基于模板)在编译时会将每个模板都展开编译成不同的数据结构。
泛型的基本语法主要有以下几种:
- 上界约束:
Data<? extends SuperType>
- 下届约束:
Data<? super BaseType>
- 并列约束:
Data<? extends BaseType & ITypeA & ITypeB>
1.1 上界约束和下届约束
在日常编码中绝大多数场景下都是用上界约束 extends
而极少见到 super
,网上对于这两个约束的区别通常解释为:
extends
:用于限定泛型类型的上界,表示类型参数必须是指定的类或其子类super
:用于限定泛型类型的下界,表示类型参数必须是指定的类或其父类
约等于废话。要理解它们的适用场景可以参考以下例子:
1 | // 假设有一个工厂负责生产多种手机,设计如下手机数据结构: |
(1)对于 createPhones
方法,PhoneFactory
接收到的集合中的对象必须满足 T extends BasePhone
,也就是说 PhoneFactory
在访问其中元素时已知每一个元素都可能会包含除了 BasePhone
以外的信息,就像 Apple
和 Samsung
一样每个手机都要添加不同的零部件。此时 PhoneFactory
只能读取其中的元素而无法添加任何元素,因为:
- 编译器无法推断存入的元素到底是
BasePhone
的哪一个子类; - 编译器可以将所有取出的元素赋值为基类
BasePhone
,等同于「将子类对象赋值给基类引用」;
这也明确了 PhoneFactory
作为消费者(消费 phones
列表)的职责。
(2)对于 getAllPhones
方法,PhoneFactory
返回的集合中的对象必须满足 T super BasePhone
,也就是说用户访问到的元素(手机)最多只能包含 BasePhone
所含有的信息(即手机的最基本功能)。此时 PhoneFactory
只能向列表中添加元素而无法读取任何元素,因为:
- 编译器可以将每个元素都视为基类后存入集合中;
- 编译器无法推断取出来的元素到底应该赋值为哪个类型,等同于「无法将基类对象赋值给子类引用」,除非使用
Object
类型引用;
这也明确了 PhoneFactory
作为生产者(生产 allPhones
列表)的职责。
当然在本例中,两个方法都可以直接使用 List<BasePhone>
代替,但这只是一种普遍存在的偷懒做法,如果有更好的规范来约束数据结构、增强代码可读性,为什么不呢?
1.2 并列约束
可能很多人此刻才知道原来泛型还能并列声明,并列泛型可以同时对类型增加多个类或接口的约束,但是当存在多个并列约束时,仅有第一个声明可以是 class
类型,此时其他的生命都必须为 interface
类型;或是全部都为 interface
类型。例如:
1 | // T 的实际类型必须同时满足以下条件:是 BaseType 或子类、并且实现了 ITypeA 和 ITypeB 接口。 |
但是要注意:当存在并列约束时,编译后的约束类型仅会保留第一个泛型声明,其他类型均是通过强转实现的:
1 | public class BaseType { |
编译以上代码之后的字节码为(省略部分):
1 | public <T extends com.example.BaseType & com.example.ITestA & com.example.ITestB> void doWithType(T); |
注意 descriptor
中只包含了 BaseType
类型,而方法实现中的 checkcast
就是强制类型转换,反编译成代码如下(省略部分):
1 | public <T extends BaseTest & ITestA & ITestB> void doTest(T testObj) { |
1.3 上界和下届嵌套约束
此外,上界约束和下届约束是可以同时存在的,例如:
1 | public <T extends Comparable<? super T>> void doWithType(T type) { |
对上例中的泛型 T
拆解:
(1)首先 T
需要满足:实现了 Comparable<? super T>
接口,注意 super
约束意味着实现的接口不能使用子类,例如:
1 | // 符合泛型约束 Comparable<? super T> |
(2)其次 T
需要满足:作为实现了 Comparable<? super T>
的类型本身或其子类,例如:
1 | // BaseType 和 SubType 均符合 T 的定义: |
当然上下界约束嵌套的情况比较少见,了解其解析规则即可。
2. 运行时泛型
泛型信息存储在类信息中,但类信息有两种载体:
- 静态类信息:
Object.class
- 对象类信息:
(new Object()).getClass()
而泛型实际上保存在 对象类信息 中,而所有类型都可以分成两种:
- Parameterized Type(参数化类型):表示含有泛型信息的类型
- Raw Type(原始类型):表示不包含泛型的原始类型
编译后泛型会被擦除成 Raw Type,但 满足一定条件 的泛型会以 Parameterized Type 的形式保存在对象类信息中,这于泛型的擦除机制有关。
2.1 泛型擦除的规则
泛型擦除遵循以下规则:
(1)无约束泛型被擦除为 Object
:
1 | // 编译前: |
(2)约束泛型被擦除为约束类型本身:
1 | // 编译前: |
(3)多约束泛型被擦除为第一个约束类型:
1 | // 编译前: |
(4)泛型容器类被擦除为原始类型(Raw Type)容器:
1 | // 编译前: |
此外,如果通过反编译去分析泛型类的字节码,会发现实际上字节码记录了泛型的详细上下文,但是这并不意味着都可以在运行时可以被 JVM 读取和使用。
2.2 泛型保留的条件
泛型类在编译后其泛型信息会被擦除为 Object,泛型会被转移到实际使用了泛型的变量或方法中(如果没有则彻底丢失存根),所以编译后的类已经丢失了自己声明的泛型信息,但可通过 Class#getGenericSuperClass()
获取父类(包括匿名类,本质上也是一种父类)携带的泛型信息。接口可以理解为一种特殊的父类,可通过Class#getGenericInterfaces()
获取接口上的泛型信息。
- 符号泛型(泛型仅以符号形式存在,没有被具体类型显式定义)在编译时被擦除;
- 参数泛型(泛型被具体类型显式定义和替换)将被保留至实例化对象的类信息中;
假设定义以下类关系:
1 | class Base<T> implements IBase<T> { ... } |
(1)符号泛型:
1 | Bypass<Short> bypass_a = new Bypass<>(); |
Bypass
上的泛型 Short
本身并没有「显式」地被传递至父类 Base
,传递的仅是泛型符号 K
,因此 bypass_a
被擦除自身泛型后将丢失 K
对应的实际类型。
(2)参数泛型:
1 | Child<Integer> child = new Child(); |
Child
在继承 Base
时「显式」地为 Base
的泛型 T
指定了 String
类型,因此实例对象 child
将丢失 Child
自身声明的泛型 Integer
,但会保留父类 Base
的泛型 String。
(3)匿名内部类:
1 | Bypass<Float> bypass_b; |
匿名内部类也是一种特殊的子类实现,因此等同于为泛型指定了实际类型。创建匿名内部类时,编译器会要求「显式」声明泛型信息(即等号右侧的泛型类型不能省略),此时的 bypass_b
是 Bypass
的匿名子类对象,因此此处的 Bypass
实际上是父类,并且编译器强制要求将泛型 Float
传递给 Bypass
,因此 bypass_b
保留了父类 Bypass
上的泛型信息。
(4)泛型嵌套:
1 | List<Base<Double>> list_a = new ArrayList<>(); |
嵌套泛型可以逐级拆解,每一级均同样遵循以上规则:
list_a
没有「显式」地将泛型传递至ArrayList
,因此list_a
丢失了泛型信息;list_b
是匿名内部类的实例对象,因此保留了所有泛型信息,包括Base
和其嵌套的Double
。需要注意:list_b.getClass()
才是真正包含了所有泛型信息的类对象;- 如果通过
list_b
获取到实际的泛型类型Base
,然后再直接对Base
获取泛型,则无法获取到嵌套的泛型Double
,因为此时获取到的Base
类不遵循「参数泛型」的规则。
3. 解析泛型
将泛型解析为实际类型是一个很常见的需求,例如事件监听:
1 | public <T extends BaseEvent> void addListener(Class<T> clz, IListener<T> listener) { |
IListener<T>
需要和泛型 T
的事件绑定注册,但由于 T
不是真实的类型,因此无法使用 T.class
或 obj instanceof T
之类的方法,导致必须另外传一个 Class<T>
用于标定 T
的实际类型,这个写法很不优雅。
3.1 解析单个泛型
从对象中解析出泛型的实际类型,已经有通用的方案:
1 | public Class<?> getGenericClass(Object obj) { |
但是该方案只能解析类似于 IListener<TheEvent>
这种最基础的泛型形式。
3.2 解析嵌套泛型
假设此时有以下两个 Listener 类型:
1 | IListener<Content<Integer>> listener_a; |
当然实际业务中这不是主流使用场景,或者也可以通过 Wrapper 来规避,但本文重在探讨如何解析。
使用上文的解析方式只能解析出外层泛型类型 Content