Java泛型

Java泛型

1. 泛型的基本定义

Java 中的泛型是 伪泛型,这是因为 Java 中的泛型会在编译期被擦除、在编译后通过强转实现类型的约束,换言之一个类不论具有多少种泛型变体,其编译后的类型都指向同一个原始类的字节码,好处是避免编译产物膨胀。而相比之下 C++ 的泛型(实际上是基于模板)在编译时会将每个模板都展开编译成不同的数据结构。

泛型的基本语法主要有以下几种:

  • 上界约束:Data<? extends SuperType>
  • 下届约束:Data<? super BaseType>
  • 并列约束:Data<? extends BaseType & ITypeA & ITypeB>

1.1 上界约束和下届约束

在日常编码中绝大多数场景下都是用上界约束 extends 而极少见到 super,网上对于这两个约束的区别通常解释为:

  • extends:用于限定泛型类型的上界,表示类型参数必须是指定的类或其子类
  • super:用于限定泛型类型的下界,表示类型参数必须是指定的类或其父类

约等于废话。要理解它们的适用场景可以参考以下例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 假设有一个工厂负责生产多种手机,设计如下手机数据结构:
public class BasePhone {
public void call() { ... }
public void unlock() { ... }
}
public class Apple extends IBasePhone {
public void addFaceId() { ... }
}
public class Samsung extends BasePhone {
public void addBoomb() { ... }
}

// 假设有一个工厂 PhoneFactory,它可以:
// - 接受一系列手机信息用于生产;
// - 给出一系列已经生产好的手机给用户使用;
// 设计如下工厂数据结构:
public class PhoneFactory {
public void createPhones(List<? extends BasePhone> phones) { ... }
public List<? super BasePhone> getAllPhones() { ... }
}

(1)对于 createPhones 方法,PhoneFactory 接收到的集合中的对象必须满足 T extends BasePhone,也就是说 PhoneFactory 在访问其中元素时已知每一个元素都可能会包含除了 BasePhone 以外的信息,就像 AppleSamsung 一样每个手机都要添加不同的零部件。此时 PhoneFactory 只能读取其中的元素而无法添加任何元素,因为:

  • 编译器无法推断存入的元素到底是 BasePhone 的哪一个子类;
  • 编译器可以将所有取出的元素赋值为基类 BasePhone,等同于「将子类对象赋值给基类引用」;

这也明确了 PhoneFactory 作为消费者(消费 phones 列表)的职责。

(2)对于 getAllPhones 方法,PhoneFactory 返回的集合中的对象必须满足 T super BasePhone,也就是说用户访问到的元素(手机)最多只能包含 BasePhone 所含有的信息(即手机的最基本功能)。此时 PhoneFactory 只能向列表中添加元素而无法读取任何元素,因为:

  • 编译器可以将每个元素都视为基类后存入集合中;
  • 编译器无法推断取出来的元素到底应该赋值为哪个类型,等同于「无法将基类对象赋值给子类引用」,除非使用 Object 类型引用;

这也明确了 PhoneFactory 作为生产者(生产 allPhones 列表)的职责。

当然在本例中,两个方法都可以直接使用 List<BasePhone> 代替,但这只是一种普遍存在的偷懒做法,如果有更好的规范来约束数据结构、增强代码可读性,为什么不呢?

1.2 并列约束

可能很多人此刻才知道原来泛型还能并列声明,并列泛型可以同时对类型增加多个类或接口的约束,但是当存在多个并列约束时,仅有第一个声明可以是 class 类型,此时其他的生命都必须为 interface 类型;或是全部都为 interface 类型。例如:

1
2
3
4
5
// T 的实际类型必须同时满足以下条件:是 BaseType 或子类、并且实现了 ITypeA 和 ITypeB 接口。
private Data<T extends BaseType & ITypeA & ITypeB> data_1;

// K 的实际类型必须同时满足以下条件:实现了 ITypeA, ITypeB, ITypeC 接口。
private Data<K extends ITypeA & ITypeB & ITypeC> data_2;

但是要注意:当存在并列约束时,编译后的约束类型仅会保留第一个泛型声明,其他类型均是通过强转实现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class BaseType {
public void doBaseType() { ... }
}
public interface ITypeA {
void doTypeA();
}
public interface ITypeB {
void doTypeB();
}

public <T extends BaseType & ITypeA & ITypeB> void doWithType(T type) {
type.doBaseType(); // 编译通过
type.doTypeA(); // 编译通过
type.doTypeB(); // 编译通过
}

编译以上代码之后的字节码为(省略部分):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public <T extends com.example.BaseType & com.example.ITestA & com.example.ITestB> void doWithType(T);
descriptor: (Lcom/example/BaseType;)V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=2, args_size=2
0: aload_1
1: invokevirtual #16 // Method com/example/BaseType.doBaseType:()V
4: aload_1
5: checkcast #17 // class com/example/ITypeA
8: invokeinterface #18, 1 // InterfaceMethod com/example/ITypeA.doTypeA:()V
13: aload_1
14: checkcast #19 // class com/example/ITestB
17: invokeinterface #20, 1 // InterfaceMethod com/example/ITestB.doTypeB:()V
22: return

注意 descriptor 中只包含了 BaseType 类型,而方法实现中的 checkcast 就是强制类型转换,反编译成代码如下(省略部分):

1
2
3
4
5
public <T extends BaseTest & ITestA & ITestB> void doTest(T testObj) {
testObj.baseTest();
((ITestA)testObj).testA();
((ITestB)testObj).testB();
}

1.3 上界和下届嵌套约束

此外,上界约束和下届约束是可以同时存在的,例如:

1
2
3
public <T extends Comparable<? super T>> void doWithType(T type) {
......
}

对上例中的泛型 T 拆解:

(1)首先 T 需要满足:实现了 Comparable<? super T> 接口,注意 super 约束意味着实现的接口不能使用子类,例如:

1
2
3
4
5
6
// 符合泛型约束 Comparable<? super T>
public class BaseType implementation Comparable<BaseType> { ... }

// 不符合泛型约束 Comparable<? super T>
public class BaseType implementation Comparable<SubType> { ... }
public class SubType extands BaseType { ... }

(2)其次 T 需要满足:作为实现了 Comparable<? super T> 的类型本身或其子类,例如:

1
2
3
// BaseType 和 SubType 均符合 T 的定义:
public class BaseType implementation Comparable<BaseType> { ... }
public class SubType extands BaseType { ... }

当然上下界约束嵌套的情况比较少见,了解其解析规则即可。


2. 运行时泛型

泛型信息存储在类信息中,但类信息有两种载体:

  • 静态类信息:Object.class
  • 对象类信息:(new Object()).getClass()

而泛型实际上保存在 对象类信息 中,而所有类型都可以分成两种:

  • Parameterized Type(参数化类型):表示含有泛型信息的类型
  • Raw Type(原始类型):表示不包含泛型的原始类型

编译后泛型会被擦除成 Raw Type,但 满足一定条件 的泛型会以 Parameterized Type 的形式保存在对象类信息中,这于泛型的擦除机制有关。

2.1 泛型擦除的规则

泛型擦除遵循以下规则:

(1)无约束泛型被擦除为 Object

1
2
3
4
5
// 编译前:
public <T> T getData() { ... }

// 编译后:
public Object getData() { ... }

(2)约束泛型被擦除为约束类型本身:

1
2
3
4
5
6
7
// 编译前:
public <T extends BaseData> void setData(T data) { ... }
public <T super BaseData> getData() { ... }

// 编译后:
public void setData(BaseData data) { ... }
public BaseData getData() { ... }

(3)多约束泛型被擦除为第一个约束类型:

1
2
3
4
5
6
7
// 编译前:
public <T extends BaseType & IType> void doWithType(T type) { ... }

// 编译后:
// 注意:反编译工具在反编译时仍然会将入参 type 自动转换为 T 类型,
// 但实际上在字节码中,方法参数定义(descriptor)的类型只有 BaseType,详见前文。
public void doWithType(BaseType type) { ... }

(4)泛型容器类被擦除为原始类型(Raw Type)容器:

1
2
3
4
5
6
7
8
9
// 编译前:
private Map<KeyType, ValueType> map;
private List<List<DataType>> nestedList;
private Set<DataType>[] setArray;

// 编译后:
private Map map; // 等同于 Map<Object, Object>
private List nestedList; // 等同于 List<Object>
private Set[] setArray; // 等同于 Set<Object>[]

此外,如果通过反编译去分析泛型类的字节码,会发现实际上字节码记录了泛型的详细上下文,但是这并不意味着都可以在运行时可以被 JVM 读取和使用。

Java Generics Type Erasure byte code

2.2 泛型保留的条件

泛型类在编译后其泛型信息会被擦除为 Object,泛型会被转移到实际使用了泛型的变量或方法中(如果没有则彻底丢失存根),所以编译后的类已经丢失了自己声明的泛型信息,但可通过 Class#getGenericSuperClass() 获取父类(包括匿名类,本质上也是一种父类)携带的泛型信息。接口可以理解为一种特殊的父类,可通过Class#getGenericInterfaces() 获取接口上的泛型信息。

  • 符号泛型(泛型仅以符号形式存在,没有被具体类型显式定义)在编译时被擦除;
  • 参数泛型(泛型被具体类型显式定义和替换)将被保留至实例化对象的类信息中;

假设定义以下类关系:

1
2
3
4
5
class Base<T> implements IBase<T> { ... }

class Bypass<K> extends Base<K> { ... }

class Child<E> extends Base<String> { ... }

(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
2
3
Bypass<Float> bypass_b;
bypass_b = new Bypass() { ... }; // ❌ 编译报错
bypass_b = new Bypass<Float>() { ... }; // ✅ 编译通过

匿名内部类也是一种特殊的子类实现,因此等同于为泛型指定了实际类型。创建匿名内部类时,编译器会要求「显式」声明泛型信息(即等号右侧的泛型类型不能省略),此时的 bypass_bBypass 的匿名子类对象,因此此处的 Bypass 实际上是父类,并且编译器强制要求将泛型 Float 传递给 Bypass,因此 bypass_b 保留了父类 Bypass 上的泛型信息。

(4)泛型嵌套:

1
2
List<Base<Double>> list_a = new ArrayList<>();
List<Base<Double>> list_b = new List<Base<Double>>() {...};

嵌套泛型可以逐级拆解,每一级均同样遵循以上规则:

  • list_a 没有「显式」地将泛型传递至 ArrayList,因此 list_a 丢失了泛型信息;
  • list_b 是匿名内部类的实例对象,因此保留了所有泛型信息,包括 Base 和其嵌套的 Double。需要注意:
    • list_b.getClass() 才是真正包含了所有泛型信息的类对象;
    • 如果通过 list_b 获取到实际的泛型类型 Base,然后再直接对 Base 获取泛型,则无法获取到嵌套的泛型 Double,因为此时获取到的 Base 类不遵循「参数泛型」的规则。

3. 解析泛型

将泛型解析为实际类型是一个很常见的需求,例如事件监听:

1
2
3
4
5
6
7
public <T extends BaseEvent> void addListener(Class<T> clz, IListener<T> listener) {
......
}

public <T extends BaseEvent> void removeListener(Class<T> clz, IListener<T> listener) {
......
}

IListener<T> 需要和泛型 T 的事件绑定注册,但由于 T 不是真实的类型,因此无法使用 T.classobj instanceof T 之类的方法,导致必须另外传一个 Class<T> 用于标定 T 的实际类型,这个写法很不优雅。

3.1 解析单个泛型

从对象中解析出泛型的实际类型,已经有通用的方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
public Class<?> getGenericClass(Object obj) {
Type superClassType = obj.getClass().getGenericSuperclass();

Type genericType = null;
if (superClassType instanceof ParameterizedType) {
genericType = ((ParameterizedType) superClassType).getActualTypeArguments()[0];
}

if (genericType != null) {
return (Class<?>) genericType;
}
return null;
}

但是该方案只能解析类似于 IListener<TheEvent> 这种最基础的泛型形式。

3.2 解析嵌套泛型

假设此时有以下两个 Listener 类型:

1
2
IListener<Content<Integer>> listener_a;
IListener<Content<String>> listener_b;

当然实际业务中这不是主流使用场景,或者也可以通过 Wrapper 来规避,但本文重在探讨如何解析。

使用上文的解析方式只能解析出外层泛型类型 Content

3.3 解析匿名类泛型


4. 通用泛型解析工具