基本设计原则

  • 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
15
16
17
18
// 类型参数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之间。
    • 在 C++ 里,f(g<a,b>(c))有两个意思:使用g<a,b>(c)的结果调用f;或者使用g<ab>(c)调用 f。所以 java 泛型参数要写在方法前面,类型见证也在方法前面。
  • List<T> 是 generic type。List<String>是 Parameterized type。

边界/绑定类型(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 的编译器做这种插入对程序的其他部分算得上是可插拔的了。

递归泛型边界模式(Recursive Generic Type Bounds)

这是被称作“F-bounded多态”(F-bounded polymorphism)或"递归泛型边界"的高级泛型设计模式。

1
2
public class RepresentationModel<T extends RepresentationModel<? extends T>> {
}
  • T必须是RepresentationModel的子类
  • 并且这个子类的泛型参数必须是T自身或T的某个子类

在这个例子里:RepresentationModel - type parameter 是 -> T <- 是子类,且这个子类的 type argument 是 T 自己 - RepresentationModel

如果不使用这种类型:

1
2
3
4
5
6
7
8
9
10
11
12
// 不使用递归泛型
public class ResourceModel {
public ResourceModel addLink(String rel, String href) {
// 添加链接的逻辑
return this;
}
}

public class UserModel extends ResourceModel {
private String username;
// 其他用户特有属性和方法...
}

那么使用时会遭遇编译错误:

1
2
3
4
5
UserModel user = new UserModel();
// 返回类型是ResourceModel,而不是UserModel
ResourceModel result = user.addLink("self", "/users/123");
// 编译错误:ResourceModel类型没有getUsername()方法
String username = result.getUsername();

这个问题的核心是,有些 fluent API 本身返回自身,这种自身不能返回子类时,父子混合 build 会产生奇怪的问题。

解法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class RepresentationModel<T extends RepresentationModel<? extends T>> {
public T addLink(String rel, String href) {
// 添加链接的逻辑
return self();
}

// 返回子类型的时候是强行转的,这一步是这类模式必须的
@SuppressWarnings("unchecked")
private T self() {
return (T) this;
}
}

public class UserModel extends RepresentationModel<UserModel> {
private String username;

public String getUsername() {
return username;
}
}

使用时:

1
2
3
4
5
UserModel user = new UserModel();
// 现在返回类型仍然是UserModel
UserModel result = user.addLink("self", "/users/123");
// 完全类型安全,编译通过
String username = result.getUsername();

这种方案的做法是:

  1. 父类所有方法都使用 T 作为 type variable。在父转子的时候,使用转型,忽略警告。
  2. 流式调用的时候都转为自己。

核心优势:

  • 类型安全的方法链:子类调用父类方法时返回的仍然是子类类型。
  • 代码复用:父类可以实现通用功能,子类无需重写这些方法。
  • 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 这样的方法能够正确接收子类类型的参数
      • 实现自引用泛型:允许枚举类引用自身类型
      • 极端重要!更复杂的例子<前边的部分,恰好是>前边的部分加上一个<?>,中间插入一个? extendsjava.lang.Object&java.lang.Serializable&java.lang.Comparable<? extends java.lang.Object&java.lang.Serializable&java.lang.Comparable<?>>
  • JPA的CriteriaBuilder
  • Stream API-易被忽略
  • 各种构建器模式实现

注意和 Comparable 比较

  1. <T extends Comparable<T>> 意味着 T 加入了一种以自己为目标的混型能力。
  2. RepresentationModel<T extends RepresentationModel<? extends T>>意味着这种类型要使用某些父子类型都共用的操作,必须让操作能够返回子类型而不是父类型。实现自引用类型层次结构
    • 类似的例子还有:public static <T> List<T> unmodifiableList(List<? extends T> list)。这种 T 总是在远处作为一个起点使用。实际的类型的多重绑定还要出现在方法参数或者返回值里。
  3. Comparable<T extends Comparable<? super T>>,意味着需要与父类型进行比较操作

交叉类型(Intersection Types)

类型参数可以同时指定多个边界,语法为 <T extends A & B & C>。规则如下:

  • 最多只能有一个类边界,且必须放在第一位
  • 接口边界可以有多个
  • 擦除后的类型为第一个边界
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.io.Serializable;

// T 必须同时是 Comparable 和 Serializable
public class Pair<T extends Comparable<T> & Serializable> {
private T first;
private T second;

public Pair(T first, T second) {
this.first = first;
this.second = second;
}

public T max() {
return first.compareTo(second) >= 0 ? first : second;
}
}

交叉类型在 Lambda 表达式中也有特殊用途——可以将 Lambda 同时转换为多个类型:

1
2
3
4
5
import java.io.Serializable;
import java.util.function.Predicate;

// Lambda 同时满足 Predicate 和 Serializable
Predicate<String> predicate = (Predicate<String> & Serializable) s -> s.length() > 0;

递归类型边界

递归类型边界(Recursive Type Bound)是指类型参数以自身为边界的模式,最典型的形式为 <T extends Comparable<T>>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.util.List;

public class RecursiveBoundExample {
// T 必须能与自身比较
public static <T extends Comparable<T>> T findMax(List<T> list) {
T max = list.get(0);
for (T element : list) {
if (element.compareTo(max) > 0) {
max = element;
}
}
return max;
}
}

递归类型边界确保了类型参数具备与自身同类型对象交互的能力。Comparable<T>compareTo 方法接受 T 类型参数,因此 T extends Comparable<T> 保证了 T 的实例可以与其他 T 实例进行比较。

自限定类型(Self-bounded Types)

自限定类型是递归类型边界的一种特殊形式,常见于基类定义中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public abstract class SelfBounded<T extends SelfBounded<T>> {
protected T self() {
@SuppressWarnings("unchecked")
T result = (T) this;
return result;
}
}

// 子类将自身作为类型参数传入
public class ConcreteType extends SelfBounded<ConcreteType> {
public ConcreteType doSomething() {
// self() 返回 ConcreteType 而非 SelfBounded
return self();
}
}

java.lang.Enum 的声明 Enum<E extends Enum<E>> 是自限定类型的经典应用。这一模式确保子类型的方法返回值和参数类型是子类型自身,而非父类型。

🔑 模式提炼:类型自约束

递归类型边界和自限定类型的核心思想是"类型参数约束自身"。当需要在基类中定义返回子类型的方法(如 Builder 链式调用、compareTo 比较)时,使用 <T extends Base<T>> 模式。Enum<E extends Enum<E>> 是 JDK 中最著名的应用。

类型擦除(type erasure)

类型擦除是 Java 泛型实现的核心机制。与 C++ 模板在编译时为每个类型参数生成独立代码不同,Java 编译器在编译后将所有泛型类型信息移除,替换为其边界类型(无边界时为 Object)。这一设计的根本原因是向后兼容:Java 5 引入泛型时,必须确保泛型代码能与 Java 1.4 及更早版本的字节码无缝互操作。

擦除遵循以下规则:

源码中的类型 擦除后的类型 说明
T(无边界) Object 最常见的情况
T extends Number Number 替换为第一个边界
T extends Comparable & Serializable Comparable 多边界取第一个
List<String> List 参数化类型擦除为原始类型
T[] Object[](或边界类型数组) 泛型数组擦除

🔑 模式提炼:擦除即退化

类型擦除的本质是编译期的类型丰富信息在运行时退化为边界类型。理解这一点是理解所有泛型限制(不能 new T()、不能 instanceof T、不能创建泛型数组)的钥匙:这些操作都需要运行时的具体类型信息,而擦除恰恰移除了这些信息。

  • 擦除的目的:为了 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>)。

擦除的实际工作流程

下图展示了类型擦除的完整流程:

flowchart TD
    %% 类型擦除流程图
    subgraph SourceCode["源代码阶段"]
        A["泛型类型定义
        public class Box~T~ {
            private T value;
            public T getValue() { return value; }
            public void setValueT value { this.value = value; }
        }"] --> B["编译器处理
        1. 类型检查
        2. 类型转换插入"]
    end
    
    subgraph ErasureProcess["类型擦除过程"]
        B --> C{类型参数 T 有边界吗?}
        C -->|无边界| D["擦除为 Object
        类型变量 T → Object"]
        C -->|有边界 T extends Number| E["擦除为边界类型
        类型变量 T → Number"]
        
        D --> F["生成桥接方法(如需要)
        保持多态性"]
        E --> F
    end
    
    subgraph Bytecode["字节码阶段(运行时)"]
        F --> G["擦除后的代码
        public class Box {
            private Object value;
            public Object getValue() { return value; }
            public void setValueObject value { this.value = value; }
            // 可能包含桥接方法
        }"]
    end
    
    subgraph Runtime["运行时"]
        G --> H["类型检查
        编译时插入的 checkcast"]
        H --> I["实际类型信息丢失
        无法获取 T 的实际类型"]
    end
    
    style SourceCode fill:#e1f5ff
    style ErasureProcess fill:#fff4e1
    style Bytecode fill:#e8f5e9
    style Runtime fill:#fce4ec
    
    note1["类型擦除是编译时过程
    泛型信息在编译后消失"] -.-> C

假设我们有一个 Pair 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Pair<T>
{
private T first;
private T second;

public Pair() { first = null; second = null; }
public Pair(T first, T second) { this.first = first; this.second = second; }

public T getFirst() { return first; }
public T getSecond() { return second; }

public void setFirst(T newValue) { first = newValue; }
public void setSecond(T newValue) { second = newValue; }
}

派生子类如下

1
2
class DateInterval extends Pair<LocalDate> {
}

按照常见的规则,编译器会把所有的方法签名都改掉,如setSecond(T newValue)会被先编译为setSecond(LocalDate newValue)

但是如果我们使用反编译器去尝试反编译这个类javap -c DateInterval.class,可以观察到如下事实:

1
2
3
4
5
6
7
8
Compiled from "Pair.java"
class com.magicliang.transaction.sys.common.util.DateInterval extends com.magicliang.transaction.sys.common.util.Pair<java.time.LocalDate> {
com.magicliang.transaction.sys.common.util.DateInterval();
Code:
0: aload_0
1: invokespecial #1 // Method com/magicliang/transaction/sys/common/util/Pair."<init>":()V
4: return
}

并不存在一个被重载的方法,什么换签名方法都不存在

而如果我们这样写:

1
2
3
4
5
6
7
class DateInterval extends Pair<LocalDate> {
public void setSecond(LocalDate second) {
if (second.compareTo(getFirst()) >0) {
super.setSecond(second);
}
}
}

子类中会出现两个 setSecond 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
Compiled from "Pair.java"
class com.magicliang.transaction.sys.common.util.DateInterval extends com.magicliang.transaction.sys.common.util.Pair<java.time.LocalDate> {
com.magicliang.transaction.sys.common.util.DateInterval();
Code:
0: aload_0
1: invokespecial #1 // Method com/magicliang/transaction/sys/common/util/Pair."<init>":()V
4: return

public void setSecond(java.time.LocalDate);
Code:
0: new #2 // class java/lang/Throwable
3: dup
4: invokespecial #3 // Method java/lang/Throwable."<init>":()V
7: astore_2
8: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
11: aload_2
12: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
15: aload_1
16: aload_0
17: invokevirtual #6 // Method getFirst:()Ljava/lang/Object;
20: checkcast #7 // class java/time/chrono/ChronoLocalDate
23: invokevirtual #8 // Method java/time/LocalDate.compareTo:(Ljava/time/chrono/ChronoLocalDate;)I
26: ifle 34
29: aload_0
30: aload_1
31: invokespecial #9 // Method com/magicliang/transaction/sys/common/util/Pair.setSecond:(Ljava/lang/Object;)V
34: return

public void setSecond(java.lang.Object);
Code:
0: aload_0
1: aload_1
2: checkcast #10 // class java/time/LocalDate
5: invokevirtual #11 // Method setSecond:(Ljava/time/LocalDate;)V
8: return
}

public void setSecond(java.time.LocalDate)是开发者定义的显式方法,而public void setSecond(java.lang.Object);是编译器合成(synthesized)的桥接方法(bridge method)

1
2
3
4
5
6
public static void main(String[] args) {
DateInterval interval = new DateInterval();
Pair<LocalDate> pair = interval;
// 调试进入可以看到多出一行隐藏栈帧,行数恰好是 class DateInterval extends Pair<LocalDate> 这一行
pair.setSecond(LocalDate.now());
}

所以总结起来:

  1. Pair<T>本质上 BoundingType 是 Object。extends Pair<LocalDate>得到的是一个所有 T 都 substitute 成 Object 的类型,作为运行时的无类型化类型使用。
  2. 但是编译器会在编译 set 方法的时候严格限制类型,如果使用了非 LocalDate 的类型设值必定发生编译错误,而如果使用 get 方法,取出来的值会隐藏一个 checkcast 的字节码指令
  3. 在调用子类实例的方法之前,会产生一个擦除类型的方法,即setSecond(java.lang.Object),在内部会有一个 checkcast 指令的实现,达到setScond((LocalDate)second)的效果。
  4. 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种方法:

  1. 反射检查:使用Method.isBridge()判断方法是否为桥接方法。Method method = DateInterval.class.getMethod("setSecond", Object.class);
  2. 字节码查看:通过javap -c DateInterval.class查看生成的桥接方法,其修饰符包含ACC_BRIDGE(本例中没有试出来)。如果直接查看 Pair.class,我们会看到方法仍然使用泛型:public void setSecond(T);桥接方法仅在子类主动重写父类泛型方法时生成

桥接方法的另一种例子

一个类型实现了 Clonable,天然是 Object 的子类。它实现自己的clone的时候,会得到两个方法:

1
2
3
4
5
// 实际方法
Employee clone() { /* 克隆逻辑 */ }
// 编译器合成的桥方法,override 了 Object.clone()
// 内部转发调用 Employee clone(),实现多态
Object clone() { return clone(); }

桥方法的生成规则

桥方法(Bridge Method)是编译器为维持类型擦除后的多态性而自动生成的合成方法。根据 JLS §13.1,桥方法具有以下特征:

  1. 生成时机:当子类覆写父类泛型方法,且类型擦除导致方法签名不一致时,编译器自动插入桥方法。
  2. 字节码标志:桥方法在字节码中带有 ACC_BRIDGEACC_SYNTHETIC 标志。
  3. 转发调用:桥方法的方法体仅包含类型转换和对实际方法的转发调用。

下图展示了桥接方法的生成和调用流程:

sequenceDiagram
    %% 桥接方法工作原理图
    participant Source as 源代码
    participant Compiler as 编译器
    participant SubClass as 子类<br/>Node
    participant Bridge as 桥接方法
    participant RealMethod as 实际方法<br/>setDataT data
    participant SuperClass as 父类<br/>MyNode
    
    Note over Source: 泛型类定义
    Source->>Compiler: class MyNode~T~ {
      setDataT data
    }
    
    Note over Source: 子类重写
    Source->>Compiler: class Node extends MyNode~Integer~ {
      setDataInteger data  // 重写
    }
    
    Compiler->>Compiler: 类型擦除
    Note right of Compiler: MyNode.setDataObject data
    
    Compiler->>SubClass: 生成桥接方法
    Note right of Compiler: 保持多态性
    
    SubClass->>Bridge: 桥接方法签名
    Note right of Bridge: public void setDataObject data {
      this.setDataInteger data  // 类型转换
    }
    
    SubClass->>RealMethod: 实际方法
    Note right of RealMethod: public void setDataInteger data {
      super.setDatadata;  // 调用父类
    }
    
    Note over SuperClass: 父类方法
    SuperClass->>SuperClass: setDataObject data
    
    rect rgb(200, 220, 240)
        Note over Bridge,SuperClass: 运行时调用流程
        Client->>SubClass: 调用 setDataObject
        SubClass->>Bridge: 桥接方法
        Bridge->>Bridge: 检查类型<br/>if data instanceof Integer
        Bridge->>RealMethod: 调用实际方法<br/>this.setDataInteger data
        RealMethod->>SuperClass: super.setDatadata
    end
    
    Note over Bridge: 桥接方法作用:
    1. 保持方法签名与父类一致
    2. 执行类型检查
    3. 委托给实际方法
1
2
3
4
5
6
7
8
9
10
11
12
import java.util.Comparator;

public class StringComparator implements Comparator<String> {
@Override
public int compare(String first, String second) {
return first.compareTo(second);
}
// 编译器生成的桥方法(反编译可见):
// public int compare(Object first, Object second) {
// return compare((String) first, (String) second);
// }
}

使用 javap -c -p StringComparator.class 可以观察到桥方法的存在。桥方法的签名与擦除后的父类方法签名一致(参数为 Object),方法体将参数强制转换为具体类型后转发给实际的覆写方法。

🔑 模式提炼:擦除桥接

桥方法是编译器为弥合"源码级泛型多态"与"字节码级擦除签名"之间的鸿沟而生成的适配器。当覆写泛型方法时,编译器自动生成一个签名与擦除后父类方法一致的桥方法,内部通过强制转换转发调用。这一机制对开发者透明,但在反射调用(Method.isBridge())和字节码分析时需要注意区分。

类型 clash

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class GenericOverloadPuzzle {
public static <T> String method(T t) {
return "Method 1";
}

public static String method(Object o) {
return "Method 2";
}

public static void main(String[] args) {
System.out.println(method("Hello"));
System.out.println(method(new Object()));
}
}

两个method本身不是重载,直接签名冲突:'method(T)' 与 'method(Object)' 冲突;两个方法具有相同的擦除

与遗留代码的冲突

把 generic class 的实例赋给 raw type class 的实例会遇到编译器警告;反过来,把 raw type class 的实例赋给 generic class 的实例也会遇到编译器警告-在跨模块传递数据时,后者往往更危险。

现代代码编写原则是:

  1. 远离早期的数据结构。
  2. 全程使用泛型类而非原始类型——除非在反射或反序列化场景中不得不获取原始类型,此时必须明确内存中的实际类型,并使用 @SuppressWarnings("unchecked") 标注。
  3. 通过不安全的类型转换,可能将 List<Integer> 赋给 List<String>,这会导致堆污染(heap pollution)

钻石操作符 <>

Java 7 引入了钻石操作符(Diamond Operator),允许编译器从赋值目标推断构造器的类型参数:

1
2
3
4
5
6
7
8
9
10
11
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class DiamondOperatorExample {
// Java 5/6:类型参数必须显式指定
Map<String, List<Integer>> oldStyle = new HashMap<String, List<Integer>>();

// Java 7+:钻石操作符,编译器从左侧推断
Map<String, List<Integer>> newStyle = new HashMap<>();
}

Java 9 进一步扩展了钻石操作符的适用范围,允许在匿名内部类中使用:

1
2
3
4
5
6
7
8
9
10
11
import java.util.Comparator;

public class DiamondWithAnonymousClass {
// Java 9+:匿名内部类也可以使用钻石操作符
Comparator<String> comparator = new Comparator<>() {
@Override
public int compare(String first, String second) {
return first.length() - second.length();
}
};
}

钻石操作符是类型推断改进的一部分,与 Type Witness 互为补充:钻石操作符简化了构造器调用,Type Witness 则在推断失败时提供显式类型指定。

Type Witness 类型见证/目击者

类型见证的抽象模式:

1
SomeClass.<TypeName>methodName(arguments)

常见使用场景:

  1. 当没有足够的上下文进行推断时

    1
    var result = Collections.<String>emptyList();
  2. 在复杂的泛型嵌套中

    1
    2
    // 假设存在 someMethod 方法
    // someMethod(Collections.<Map<String, Integer>>emptyList());
  3. 在 Java 7 及以前:这行代码可能不能编译,因为 processStringList 的参数可能是个泛型类型,而实参赋值的时候无法推导列表类型:

    1
    2
    // 假设存在 processStringList 方法
    // processStringList(Collections.emptyList());

其中第二种情况更常见,如果方法调用左边有足够的目标类型,编译器不会产生有歧义的推导(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()

不能实例化泛型数组

T[] a = new T[1] // error

如果我们需要得到某种类型的数组,可以采取如下方法:

  1. 先找到一个泛型 List,然后使用下文提到的“空泛型数组的拷贝方法”。
  2. 想办法生成一个数组,然后(T[])做转型-实例化不可以,但是转型可以

这一条和上面的 ParameterizedType 条目差不多。

泛型类的 type variables 不能在它的 static 上下文里工作

但静态方法自己可以有 type variables。

我们可以声明一个class Single<T>,但这个Single里的静态方法不能引用T。静态方法里可以自己声明另一个<U>

不能抛出和捕获泛型类

  1. T extends Throwable合法。
  2. method throws T合法,throw (T)t合法。
  3. catch(T t)非法。
  4. Problem<T> extends Exception非法。

巧妙地使用泛型,可以破坏 checked exception 的限制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
interface Task
{
void run() throws Exception;

@SuppressWarnings("unchecked")
static <T extends Throwable> void throwAs(Throwable t) throws T
{
throw (T) t;
}

static Runnable asRunnable(Task task)
{
return () ->
{
try
{
task.run();
}
catch (Exception e)
{
// 编译器允许我们去掉 <RuntimeException>,似乎可以直接抛出运行时异常
Task.<RuntimeException>throwAs(e);
}
};
}
}

public class DefeatCheckedExceptionChecking
{
public static void main(String[] args)
{
var thread = new Thread(Task.asRunnable(() ->
{
Thread.sleep(1000);
System.out.println("Hello, World!");
throw new Exception("Check this out!");
}));
thread.start();
}
}

我们骗过了编译器,让它认为asRunnable抛出的不是受检异常:

  1. 调用 asRunnable 以后,我们不需要写受检异常的 handler,也不需要new RuntimeException(ex)来 wrap
  2. 运行时throw (T) t;不会报错,因为擦除是擦除到 Throwable,我们仍然在更上游遇到这个异常时和遇到普通异常的流程是一致的。

永远不要写boolean equals(T object)

因为每个泛型方法都有一个兜底的 raw type 方法兜底,如果兜底方法和父类的非泛型方法相冲突(clash),编译会报错。

编译器会擦除出 boolean equals(Object object),制造同签名的方法,连重载都算不上。

只能写:

1
2
3
4
5
6
@Override
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false;
Pair<?> pair = (Pair<?>) o;
return Objects.equals(first, pair.first) && Objects.equals(second, pair.second);
}

这样构成了显式override。

所以这里有一个极端重要的认知:

  1. 我们不能在泛型类里声明擦除后有同签名的方法。foo(T o)foo(Object o)不算一种重载,算一种 clash。但我们可以写@override foo(Object o){}来 override。
  2. 我们可以在实例化的子类里面写具体类型来 overrload 擦除方法,编译器会帮我们把overload转成 override。例子见上面“擦除的实际工作流程”。

再谈数组和泛型类的继承和协变

数组隐藏有类型信息。所以虽然数组是协变的,但是

1
2
3
4
5
6
7
8
// 数组协变与继承示例
class A {}
class B extends A {}

// 合法:子类数组可以赋值给父类数组引用
A[] arr = new B[10];
// 但是 arr 实际上是一个 B 数组
// arr[0] = new A(); // 运行时抛出 ArrayStoreException,B 数组不允许存储 A 类型

但是泛型类是完全擦除的,所以对于 A List 里插入 B 元素,如果允许继承,则真的会发生混合插入的问题。

Java 有一个习惯,就是不完全防止堆污染,有时候靠在取值的时候抛出异常来解决。

泛型与数组

数组协变 vs 泛型不变

Java 数组是协变的(covariant):如果 BA 的子类型,则 B[]A[] 的子类型。这一设计在 Java 1.0 时代为多态数组操作提供了便利,但也引入了运行时类型安全风险:

1
2
3
// 数组协变示例(注意:42 会被自动装箱为 Integer)
Object[] objectArray = new String[10]; // 编译通过:数组协变
// objectArray[0] = 42; // 编译通过,运行时抛出 ArrayStoreException

泛型则是不变的(invariant):List<String> 不是 List<Object> 的子类型。这一设计将类型错误从运行时提前到编译时:

1
2
// List<Object> list = new ArrayList<String>(); // 编译错误
List<? extends Object> list = new ArrayList<String>(); // 通配符提供受限的协变

为什么不能创建泛型数组

new T[]new List<String>[10] 都是非法的。根本原因在于类型擦除与数组的具化(reification)特性冲突:

  • 数组是具化的:数组在运行时知道自己的元素类型,并在每次赋值时执行类型检查(ArrayStoreException)。
  • 泛型是擦除的:泛型类型参数在运行时不存在,无法为数组提供运行时类型检查所需的信息。

如果允许创建泛型数组,将导致类型安全漏洞:

1
2
3
4
5
// 假设以下代码合法(实际编译不通过)
// List<String>[] stringLists = new List<String>[1]; // 假设合法
// Object[] objects = stringLists; // 数组协变,合法
// objects[0] = List.of(42); // 运行时无法检测类型错误
// String value = stringLists[0].get(0); // ClassCastException

替代方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.List;

public class GenericArrayFactory {
// 方案一:通过 Array.newInstance 创建
@SuppressWarnings("unchecked")
public static <T> T[] createArray(Class<T> componentType, int length) {
return (T[]) Array.newInstance(componentType, length);
}

// 方案二:使用集合替代数组
public static <T> List<T> createList() {
return new ArrayList<>();
}
}

🔑 模式提炼:具化与擦除的冲突

数组的协变+具化设计与泛型的不变+擦除设计存在根本矛盾。当需要"泛型容器"时,优先使用 List<T> 而非 T[]。当确实需要泛型数组时,通过 Array.newInstance() 配合 Class<T> 令牌在运行时创建。

泛型能够做什么

  • type variable 可以拿来做目标参数,如T t = get()(T)(T[])
  • 我们始终可以把 parameterized type 往 raw type 转。
  • ArrayList<Integer>可以往List<Integer>转-我们经常是这样声明的。

通配符(wildcard)

  1. ? 表明了 Type variable 是怎样 vary的。
  2. 泛型不是协变的,但 ? 用来实现某些人类似协变的需求,产生了一个泛型类家族的父类/子类(通配符允许泛型类的继承树在泛型层面出现,而普通泛型则只允许泛型类的继承树在外部类的层面出现)。这增加代码的灵活性,同时保持类型安全。
1
2
3
4
5
List<?> (根节点)
├── List<? extends Number> // 子类型范围:?可以是Number或其子类(如IntegerDouble
│ └── List<Integer> // 具体类型(非通配符)
└── List<? super Integer> // 子类型范围:?可以是Integer或其父类(如NumberObject
└── List<Number> // 具体类型(非通配符)

反直觉的两种子类关系,子类意味着类型兼容

  1. Pair<? extends Employee>意味着它的子类 可以是Pair<Employee>Pair<Manger>
  2. Pair<? super Employee>意味着它的子类(而不是超类)可以是Pair<Employee>Pair<Object>

PECS 原则

编译器在编译时并不会真的把 ? 替换为具体类型——虽然开发者经常在概念上这样想

PECS 讨论的是,Employee 作为目标类型,什么时候只能消费-作为参数/右值,什么时候只能生产-作为返回值/左值

? extends T作为一个 type parameter 证明可以在 T t 处读
? super T 作为一个 type parameter 证明可以在 T t 处写

这个原则很好地示范了**定义(简单性)-例子(复杂性)-助记符(简单性)**之间的关系。

1
2
3
4
5
6
7
8
9
10

public void foo(Collection<? extends Number> e) {
// 编译错误,因为e可能 Collection<Integer> 或者 Collection<Long>
e.add(1);
}

// 合法
public Collection<? extends Number> bar() {
return new ArrayList<Integer>();
}

由于编译器无法确定具体类型,所以干脆不允许 set/call(消费),但可以get。

1
2
3
4
5
6
7
8
9
public void foo(Collection<? super Number> e) {
// 合法,因为 1 可以被加入到 Collection<Number> 和 Collection<Object> 里
e.add(1);
}

// 非法,因为这里只允许赋值给 Collection<Number> 和 Collection<Object>
public Collection<? super Number> bar() {
return new ArrayList<Integer>();
}

用赋值和设值,用 collection 的 get 和 add 比较好理解这个规则。

🔑 模式提炼:读写分离边界

PECS 的本质是泛型的读写分离:? extends T 保证读出的值至少是 T(安全读取),? super T 保证写入的 T 值一定兼容容器类型(安全写入)。设计 API 时,参数如果只读就用 extends,只写就用 super,既读又写就用精确类型 T。

下图展示了通配符类型的关系和 PECS 原则:

graph LR
    %% 通配符继承关系与 PECS 原则
    subgraph WildcardTypes["通配符类型"]
        A["? 无界通配符
        Unknown Type"]
        B["? extends T 上界通配符
        Covariant (协变)"]
        C["? super T 下界通配符
        Contravariant (逆变)"]
    end
    
    subgraph PECS["PECS 原则"]
        P["Producer Extends
        生产者使用 extends"]
        C2["Consumer Super
        消费者使用 super"]
    end
    
    subgraph Examples["使用示例"]
        E1["List~? extends Number~
        只能读取不能写入
        Number n = list.get0;"]
        E2["List~? super Integer~
        只能写入不能读取
        list.addnew Integer1;"]
    end
    
    subgraph Inheritance["继承关系"]
        I1["Integer <: Number"]
        I2["List~Integer~ <: List~? extends Integer~"]
        I3["List~Integer~ <: List~? super Integer~"]
    end
    
    A -.->|"可以视为"| B
    A -.->|"可以视为"| C
    
    B -->|"适用于"| P
    C -->|"适用于"| C2
    
    P --> E1
    C2 --> E2
    
    style WildcardTypes fill:#e3f2fd
    style PECS fill:#f3e5f5
    style Examples fill:#e8f5e9
    style Inheritance fill:#fff3e0
    
    note["? extends T: 只读,不能写入
    ? super T: 只写,不能读取确型"] -.-> B

日期的例子-?和 T 同时出现

LocalDateChronoLocalDate的子类(顺序就是这样,没有反过来),但 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 type),其 setFirst(? object) 方法无法被调用——根据 PECS 法则,编译器无法确定传入的值是否兼容未知的通配符类型。而 getFirst 方法的返回值也只能赋给 Object

Pairset 方法可以使用任意类型,而 get 方法可以赋给 Object

如果某些场景只需从 Pair<T> 内部读值,那么 Pair<?>Pair<T> 更加简洁易读。

通配符捕获:单层<?>参数方法套着单层<T>参数方法,而逻辑是在内层方法里写的

? 不是 type variable,是 wildcard variable。

通配符捕获要解决的问题是:无法用 ? 来写代码,不能当作左值的类型来用。? a = Pair.getFirst() 是不合法的。因此无法编写 swap 方法:

通配符捕获的解法是:引入一个 T 来捕获通配符,用 T 来写代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

public class WildcardCapture {
// 辅助泛型方法:捕获通配符类型
private static <T> void swapHelper(List<T> list, int i, int j) {
// 在方法体内可以随意使用 temp。用有值的实例来获取泛型变量。
T temp = list.get(i);
list.set(i, list.get(j));
list.set(j, temp);
return temp;

}

// 对外暴露的通配符方法
public static void swap(List<?> list, int i, int j) {
Object o = swapHelper(list, i, j); // 通配符捕获在此发生
}
}
// 甲骨文的例子
public class WildcardFixed {

void foo(List<?> i) {
fooHelper(i);
}

// Helper method created so that the wildcard can be captured
// through type inference.
private <T> void fooHelper(List<T> l) {
l.set(0, l.get(0));
}

}

在这里,T 捕获了通配符。T 不知道 ? 的具体类型,但知道它是某个确定类型-就当 Object 用。

编译器只有在确定 T 可以捕获确定的通配符的时候才允许编译通过。例如List<Pair<?>> 多了一个间接层,一个 list 可能有不同的 Pair<?>(这足以证明?即使是在一个数据结构里,也可能产生多个类型Pair1<?>内部的的?是同一种?,但是 Pair2<?>完全是另一种),持有不同的具体类型,编译器不会允许捕获 List<Pair<T>>里的 T。

1
2
3
4
5
6
7
8
9
10
11
12
public class WildcardFixed {

void foo(List<List<?>> i) {
fooHelper(i);
}

// not working
private <T> void fooHelper(List<List<T>> l) {
l.set(0, l.get(0));
}

}

需要的类型: List <List<T>> 提供的类型: List <List<?>> 原因: 不兼容的相等约束: T 和 ?

🔑 模式提炼:通配符捕获桥接

当需要对通配符类型执行读写操作时,编写一个私有泛型辅助方法来"捕获"通配符:外层方法接受 <?>,内层方法用 <T> 捕获具体类型。编译器通过类型推断将 ? 绑定到 T,从而在辅助方法内部获得完整的类型操作能力。注意:多层嵌套的通配符(如 List<List<?>>)无法被捕获,因为内层的 ? 可能代表不同的具体类型。

幽灵类型

例子来自于《Java中的幽灵类型》《你真的懂 Builder 设计模式吗?论如何实现真正安全的 Builder 模式》

忽略了特别复杂的阶段式构造,因为它是与泛型无关的。

构造类型的用法

  1. 用接口子类当作状态,而不是枚举,这样可以作为 generic 的bound使用。
  2. 定义泛型类,泛型类只有私有构造器,不允许外部创建,因为这是泛型类,所以构造器在调用的时候是一定可以带有类型见证的,如new Plane<Landed>(10)
  3. 定义初始构造方法,只允许生成初始态实例。
  4. **关键:**定义复制构造方法,允许? extend FlightStatus这个类型族的实例相互拷贝,这样产生了外层方法。
  5. 定义各种跳转方法, 关键:在签名和返回值上做幽灵类型的切换,内部只使用同一个参数族方法复制来复制去,每次复制的时候使用新的类型见证return new Plane<Flying>(p);

不基于接口的用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import java.util.List;
import java.util.ArrayList;

class Member {}

public class Team<S> {
private List<Member> members = new ArrayList<>();
// ...其他代码
// phantom types
static abstract class READY {}
static abstract class STARTED {}
static abstract class END {}
}

public class Game {
public Team<Team.STARTED> start(Team<Team.READY> team) {
// ...实现代码
return null;
}
public Team<Team.END> end(Team<Team.STARTED> team) {
// ...实现代码
return null;
}
// ...其他方法
}

builder pattern

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class User {
private final String name;
private final String password;

private User(String name, String password) {
this.name = name;
this.password = password;
}

public static User build(Builder<TRUE, TRUE> builder) {
return new User(builder.name, builder.password);
}

public static Builder<FALSE, FALSE> builder() {
return new Builder<FALSE, FALSE>();
}

public static class Builder<HNAME, HPASSWORD> {
private String name;
private String password;

private Builder() {
}

private Builder(String name, String password) {
this.name = name;
this.password = password;
}

public Builder<TRUE, HPASSWORD> name(String name) {
this.name = name;
return new Builder<TRUE, HPASSWORD>(name, this.password);
}

public Builder<HNAME, TRUE> password(String password) {
this.password = password;
return new Builder<HNAME, TRUE>(this.name, password);
}
}
// phantom types
static abstract class TRUE {}
static abstract class FALSE {}
}
  1. 把 builder 泛型化,每个字段对应一个泛型参数。
  2. 定义 TRUE、FALSE 两个状态。
  3. 构造器要求字段对应的泛型参数全部都是 TRUE。
  4. 每个 builder 只保留一个本参数对应的类型实参,其他部分仍然泛化-部分泛型
  5. 如果一个字段有默认值,它不具有对应的泛型参数。

🔑 模式提炼:编译期状态机

幽灵类型将运行时状态检查提前到编译期:用不同的类型(而非枚举值)表示状态,用泛型参数携带状态信息,用方法签名的类型变换表达状态转移。非法的状态转移在编译期就会报错,而非等到运行时才抛出异常。这一模式特别适合 Builder、状态机、协议流程等需要强制执行调用顺序的场景。

泛型与反射

泛型与 class

String.classClass<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 接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public abstract class TypeToken<T> {
private final Type type;

public TypeToken() {
Type superClass = getClass().getGenericSuperclass();
this.type = ((ParameterizedType) superClass).getActualTypeArguments()[0];
}

public Type getType() { return type; }
}

// 使用示例(假设 TypeToken 类已定义)
// TypeToken<List<String>> token = new TypeToken<List<String>>() {};
// System.out.println(token.getType()); // 输出: java.util.List<java.lang.String>

🔑 模式提炼:匿名子类类型捕获

TypeToken 利用了一个关键事实:虽然泛型在运行时被擦除,但类定义中的泛型信息会保留在字节码的 Signature 属性中。通过创建匿名子类 new TypeToken<List<String>>() {},泛型参数 List<String> 被固化到子类的类定义中,可通过 getGenericSuperclass() 在运行时获取。Gson、Jackson、Spring 的 ParameterizedTypeReference 都基于这一原理。

反射的类型 hierarchy

java类型系统.png

下图展示了 Java 反射 Type 接口的完整层次结构:

classDiagram
    %% Java 反射 Type 接口层次结构图
    class Type {
        <<interface>>
        Type 类型表示
    }
    
    class Class {
        <<class>>
        表示原始类型和泛型类
        +getTypeParameters()
        +getComponentType()
    }
    
    class TypeVariable {
        <<interface>>
        表示类型变量(如 T, E)
        +getName()
        +getBounds()
        +getGenericDeclaration()
    }
    
    class ParameterizedType {
        <<interface>>
        表示参数化类型(如 List~String~)
        +getActualTypeArguments()
        +getRawType()
        +getOwnerType()
    }
    
    class GenericArrayType {
        <<interface>>
        表示泛型数组类型(如 T[])
        +getGenericComponentType()
    }
    
    class WildcardType {
        <<interface>>
        表示通配符类型(如 ? extends T)
        +getUpperBounds()
        +getLowerBounds()
    }
    
    Type <|-- Class
    Type <|-- TypeVariable
    Type <|-- ParameterizedType
    Type <|-- GenericArrayType
    Type <|-- WildcardType
    
    note for Type "java.lang.reflect.Type\n所有类型的顶级接口"
    note for Class "表示具体的类类型\n如 String, List, Integer"
    note for TypeVariable "泛型类型变量\n如 T, E, K, V"
    note for ParameterizedType "参数化类型\n如 List~String~, Map~String, Integer~"
    note for GenericArrayType "泛型数组\n如 T[], List~E~[]"
    note for WildcardType "通配符类型\n如 ?, ? extends Number, ? super String"

反射的基础类型是Type接口,它有五个子类:

Class

描述具体类型(而不是接口)。我们常见的类型如果本身不带有<>就属于这一类型。对 Java 1.5 以后的泛型,一个Map.classMap<String, Integer>的 class 在求等上完全相等,但唯有引入其他 Type 子类才能说明StringInteger的存在。

  • 最常见的 Type 实现类。
  • 代表原始类型(raw types)和基本类型(primitive types)
1
2
Class<?> clazz = String.class;
Class<?> arrayClass = int[].class; // class [I

TypeVariable(类型变量)

  • 表示泛型中的类型变量
  • 例如泛型定义中的 T、K、V 等
1
2
3
4
5
6
7
8
public class Container<T> {
private T value;

public void showType() {
TypeVariable<?>[] typeParameters = Container.class.getTypeParameters();
// typeParameters[0] 就是 T
}
}

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
2
3
4
5
6
7
8
9
10
11
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

// HashMap 的 getGenericSuperclass() 返回的是 AbstractMap,不是参数化类型
// 这里仅演示反射 API 的使用
Map<String, List<Integer>> map = new HashMap<>();
Type type = map.getClass().getGenericSuperclass();
// 注意:以下 instanceof 检查实际不会通过,因为 HashMap 的父类不是参数化类型
// 实际使用时需要先判断 Type 的具体类型,再进行相应的类型转换和操作

GenericArrayType(泛型数组类型)

GenericArray 接口,描述泛型数组如 T[]。这可以被用在获取方法参数上。获取到具体参数以后,可以把它转化为 ParameterizedType,然后获取它携带的 actualParamters。例如:T[]List<String>[]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.lang.reflect.Field;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.List;

public class Example<T> {
T[] array;
List<String>[] lists;

public void check() throws NoSuchFieldException {
Field arrayField = Example.class.getDeclaredField("array");
Type arrayType = arrayField.getGenericType();
if (arrayType instanceof GenericArrayType) {
Type componentType = ((GenericArrayType) arrayType).getGenericComponentType();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

### WildcardType(通配符类型)

表示通配符泛型,如 `?`、`? extends Number`、`? super Integer`。它必然可以找到 up bound(最起码有 Object),不一定有 lower bound。

```java
public class WildcardExample {
List<? extends Number> numbers;

public void examine() {
Field field = WildcardExample.class.getDeclaredField("numbers");
Type type = field.getGenericType();
if (type instanceof ParameterizedType) {
Type[] actualTypeArguments = ((ParameterizedType) type).getActualTypeArguments();
WildcardType wildcard = (WildcardType) actualTypeArguments[0];
Type[] upperBounds = wildcard.getUpperBounds(); // 获取上界
Type[] lowerBounds = wildcard.getLowerBounds(); // 获取下界
}
}
}

主要使用场景

  1. 反射获取泛型信息
  2. 泛型类型系统的实现
  3. 框架开发中的类型判断和处理
  4. 注解处理器的开发
  5. 序列化/反序列化框架

这些类型系统的实现让 Java 能够在运行时获取和处理泛型信息,尽管 Java 的泛型是通过类型擦除实现的,但通过这些接口仍然可以在运行时获取到泛型的类型信息。

🔑 模式提炼:Type 五子类分治

处理反射中的泛型类型时,采用分治策略:对 Type 实例依次判断是否为 Class(具体类型)、ParameterizedType(参数化类型)、TypeVariable(类型变量)、WildcardType(通配符)、GenericArrayType(泛型数组),分别提取对应的类型信息。这五种子类型覆盖了 Java 类型系统的所有可能形态。

TypeLiteral

有一个可以捕获多重泛型的实参的方案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68

// 1,当我们拿到一个Class,用Class. getGenericInterfaces()方法得到Type[],也就是这个类实现接口的Type类型列表。
// 2,当我们拿到一个Class,用Class.getDeclaredFields()方法得到Field[],也就是类的属性列表,然后用Field. getGenericType()方法得到这个属性的Type类型。
// 3,当我们拿到一个Method,用Method. getGenericParameterTypes()方法获得Type[],也就是方法的参数类型列表。

/**
* This constructor must be invoked from an anonymous subclass
* as new TypeLiteral<. . .>(){}.
*/
public TypeLiteral()
{
//
Type parentType = getClass().getGenericSuperclass();
if (parentType instanceof ParameterizedType)
{
// 重要:这里得到的 Type 是真的 Class 实例,不过做类型判定不能用 instanceof,只能用 assignableFrom。因为 Class 的自身类型和指代类型是不一样的。当然 Type 也可能是 ParameterizedType 等任意子类型,它们也可以通过 getClass 理解自身的类型。
type = ((ParameterizedType) parentType).getActualTypeArguments()[0];
}
else
throw new UnsupportedOperationException(
"Construct as new TypeLiteral&lt;. . .&gt;(){}");
}

// 上述 api 的例子
public static void main(String[] args) {
List<String> list = new ArrayList<>();
Class<? extends List> firstClazz = list.getClass();
Type genericSuperclass = firstClazz.getGenericSuperclass();
// java.util.AbstractList<E>
System.out.println(genericSuperclass);

ParameterizedType parameterizedSuperType = (ParameterizedType) genericSuperclass;
Type[] actualTypeArguments = parameterizedSuperType.getActualTypeArguments();
// E
Type actualTypeArgument = actualTypeArguments[0];
System.out.println(actualTypeArgument);

// java.util.AbstractCollection<E>
genericSuperclass = firstClazz.getSuperclass().getGenericSuperclass();
System.out.println(genericSuperclass);

parameterizedSuperType = (ParameterizedType) genericSuperclass;
actualTypeArguments = parameterizedSuperType.getActualTypeArguments();
// E
actualTypeArgument = actualTypeArguments[0];
System.out.println(actualTypeArgument);
}

// 但这个方法有一个例外,如:

private static class StringList extends ArrayList<String> {
}

public static void main(String[] args) {
List<String> list = new StringList();
Class<? extends List> firstClazz = list.getClass();
Type genericSuperclass = firstClazz.getGenericSuperclass();
// java.util.ArrayList<java.lang.String>
System.out.println(genericSuperclass);

ParameterizedType parameterizedSuperType = (ParameterizedType) genericSuperclass;
Type[] actualTypeArguments = parameterizedSuperType.getActualTypeArguments();
// class java.lang.String 类型不再是 typevariable
Type actualTypeArgument = actualTypeArguments[0];
System.out.println(actualTypeArgument);
}

// 这个例外下面有用。

运行时捕获类型参数的方法

方法一

1
2
3
4
5
6
7
8
9
10
11
12
public class GenericClass<T> {

private final Class<T> type;

public GenericClass(Class<T> type) {
this.type = type;
}

public Class<T> getMyType() {
return this.type;
}
}

方法二

typetools

方法三

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public abstract class TypeReference<T> implements Comparable<TypeReference<T>>
{
protected final Type _type;

protected TypeReference()
{
Type superClass = getClass().getGenericSuperclass();
if (superClass instanceof Class<?>) { // sanity check, should never happen
throw new IllegalArgumentException("Internal error: TypeReference constructed without actual type information");
}
/* 22-Dec-2008, tatu: Not sure if this case is safe -- I suspect
* it is possible to make it fail?
* But let's deal with specific
* case when we know an actual use case, and thereby suitable
* workarounds for valid case(s) and/or error to throw
* on invalid one(s).
*/
_type = ((ParameterizedType) superClass).getActualTypeArguments()[0];
}
}

这个方法来自于 JDK 5 的作者的博客《super-type-tokens》

  1. abstract class 保证了这个类型必须通过子类确定,这样getGenericSuperclass必定会得到一个 ParameterizedType 而不仅仅是一个 GenericType。
  2. implements Comparable<TypeReference<T>> 并不是真的希望子类覆写一个比较方法,而是希望子类型不要实现成一个 raw type。

保证了这两天_type一定是一个 concret class。

泛型的重载限制

由于类型擦除,以下重载在 Java 中是非法的:

1
2
3
4
5
// 编译错误:两个方法擦除后签名相同
public class OverloadExample {
public void process(List<String> strings) { }
// public void process(List<Integer> integers) { } // 擦除后都是 process(List)
}

擦除后 List<String>List<Integer> 都变为 List,导致方法签名冲突。解决方案:

  1. 使用不同的方法名processStrings(List<String>)processIntegers(List<Integer>)
  2. 使用通配符统一process(List<? extends CharSequence>)
  3. 添加额外参数区分:利用幽灵类型参数(参见本文"幽灵类型"章节)

各种泛型黑魔法

类似幽灵类型的模式匹配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.util.ArrayList;
import java.util.List;

class A {}
class B {}

class X {
void call(List<A> ls, A... dummy) {
System.out.println("call A");
}
void call(List<B> ls, B... dummy) {
System.out.println("call B");
}
}

// 使用示例
public class Main {
public static void main(String[] args) {
var x = new X();
x.call(new ArrayList<A>());
x.call(new ArrayList<B>());
}
}

获取通配符的 bound

此处并非在代码中使用通配符,而是确切知道?的 bound 是什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.WildcardType;
import java.util.Arrays;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Assertions;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class TestClass {

// 任何相关的问题都可以写一个成员类来直接通过反射来实验
private List<? extends Number> listNum;

@Test
@SuppressWarnings("unchecked")
void testGetWildCardType() throws NoSuchFieldException {

final Field fieldNum = TestClass.class.getDeclaredField("listNum");
// genericType几乎必然是 ParameterizedTypeImpl 的实例
final Type genericType = fieldNum.getGenericType();
final ParameterizedType parameterizedType = (ParameterizedType) genericType;
final Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
for (Type actualTypeArgument : actualTypeArguments) {
// wildCard 隐藏在 actualTypeArguments 里
if (actualTypeArgument instanceof WildcardType) {
final WildcardType wildcardType = (WildcardType) actualTypeArgument;
final Type[] lowerBounds = wildcardType.getLowerBounds();
// 所有的 bound 都是 class
log.info(Arrays.toString(lowerBounds));
final Type[] upperBounds = wildcardType.getUpperBounds();
log.info(Arrays.toString(upperBounds));
}
}

Assertions.assertTrue(true);
}
}

Optional 里通配符转成类型参数

可见@SuppressWarnings("unchecked") java 的基础库里到处都是。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.util.Optional;

public class OptionalExample {
/**
* 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;

public class GenericUtils {
/**
* 验证一个类型是不是另一个类型的参数化子类
* <p>
* A implements B<C>
* 则 isGenericSubClass(A.class, B.class, C.class) 为 true
* 注意,如果存在 D extends C,C extends F
* D 作为第三个参数返回 true,而 F 返回 false
*
* @param subClass 子类型
* @param genericSuperClass 超类型,可以是接口
* @param targetGenericActualParameter 类型实参
* @return 认定结果
*/
public static boolean isGenericSubClass(Class<?> subClass, Class<?> genericSuperClass, Class<?> targetGenericActualParameter) {

if (!genericSuperClass.isAssignableFrom(subClass)) {
return false;
}
final Type genericSuperclass = subClass.getGenericSuperclass();

if (genericSuperclass instanceof ParameterizedType) {
final ParameterizedType parameterizedType = (ParameterizedType) genericSuperclass;
final Type rawType = parameterizedType.getRawType();
// 这里可以确认 Collection 是不是 Collection-而不是 List
final boolean rawTypeMatch = rawType == genericSuperClass;
final Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
// 这两行很重要,类型实参的获取主要依赖此方法,但获取到的类型仅限于 Class 实例
for (Type type : actualTypeArguments) {
if (type instanceof Class) {
if (rawTypeMatch && ((Class<?>) type).isAssignableFrom(targetGenericActualParameter)) {
return true;
}
}
}
}

final Type[] genericInterfaceTypes = subClass.getGenericInterfaces();
for (Type genericInterfaceType : genericInterfaceTypes) {
if (genericInterfaceType instanceof ParameterizedType) {
final ParameterizedType parameterizedType = (ParameterizedType) genericInterfaceType;
final boolean rawTypeMatch = parameterizedType.getRawType() == genericSuperClass;
final Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
// 这两行很重要,类型实参的获取主要依赖此方法,但获取到的类型仅限于 Class 实例
for (Type type : actualTypeArguments) {
if (type instanceof Class) {
if (rawTypeMatch && ((Class<?>) type).isAssignableFrom(targetGenericActualParameter)) {
return true;
}
}
}
}
}
return false;
}

/**
* isGenericSubClass 的 Spring 版本,只能验证第一个 TypeParameter,验证第二个参数必然返回 false
*
* @param subClass 子类型
* @param genericSuperClass 超类型,可以是接口
* @param targetGenericActualParameter 类型实参
* @return 认定结果
*/
public static boolean isParameterized(Class<?> subClass, Class<?> genericSuperClass, Class<?> targetGenericActualParameter) {
// 需要引入 Spring 的 ResolvableType
// final ResolvableType generic = ResolvableType.forClass(subClass).as(genericSuperClass).getGeneric();
// return generic.isAssignableFrom(ResolvableType.forClass(targetGenericActualParameter));
// 此处仅为示例,实际使用需要 Spring 依赖
return false;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

## 空泛型数组的拷贝方法

```java
import java.util.Arrays;

public class ArrayCollection<E> {
private final E[] a;

public ArrayCollection(E[] array) {
this.a = array;
}

public int size() {
return a.length;
}

@Override
@SuppressWarnings("unchecked")
public <T> T[] toArray(T[] a) {
int size = size();
if (a.length < size)
return Arrays.copyOf(this.a, size,
(Class<? extends T[]>) a.getClass());
System.arraycopy(this.a, 0, a, 0, size);
if (a.length > size)
a[size] = null;
return a;
}
}

这个方法的特点:

  1. 自动调整数组大小。
  2. 可以像 go 一样往参数填充数据。
  3. 类型安全:String[] rightWay = list.toArray(new String[0]);优于String[] wrongWay = (String[]) list.toArray();

泛型在 Lambda 与 Stream 中的类型推断

Java 8 引入的 Lambda 表达式和 Stream API 大量依赖泛型类型推断。编译器通过目标类型推断(Target Type Inference)确定 Lambda 表达式的类型参数。

目标类型推断

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.util.function.Function;
import java.util.function.Predicate;

public class TargetTypeInference {
// 编译器从赋值目标推断 T=String, R=Integer
Function<String, Integer> lengthFunction = s -> s.length();

// 编译器从方法参数类型推断 T=String
Predicate<String> nonEmpty = s -> !s.isEmpty();

// 方法引用同样依赖目标类型推断
Function<String, Integer> parseFunction = Integer::parseInt;
}

Stream 链式调用中的类型传播

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class StreamTypeInference {
public static void main(String[] args) {
List<String> names = List.of("Alice", "Bob", "Charlie");

// 类型参数沿 Stream 管道逐步推断
// Stream<String> → Stream<Integer> → List<Integer>
List<Integer> lengths = names.stream()
.map(String::length) // 推断 Function<String, Integer>
.filter(length -> length > 3) // 推断 Predicate<Integer>
.collect(Collectors.toList());

// 复杂的类型推断:分组操作
// 推断 Collector<String, ?, Map<Integer, List<String>>>
Map<Integer, List<String>> groupedByLength = names.stream()
.collect(Collectors.groupingBy(String::length));
}
}

当类型推断失败时,可以通过类型见证(Type Witness)显式指定:

1
2
3
4
5
6
7
8
9
10
import java.util.Collections;
import java.util.List;

public class TypeWitnessExample {
public static void main(String[] args) {
// 编译器无法推断空集合的类型参数
// List<String> list = Collections.emptyList(); // 在某些上下文中可能失败
List<String> list = Collections.<String>emptyList(); // 类型见证
}
}

🔑 模式提炼:目标类型驱动

Lambda 和 Stream 的类型推断遵循"目标类型驱动"原则:编译器从表达式的使用上下文(赋值目标、方法参数、返回类型)反向推断类型参数。当推断失败时,使用类型见证 ClassName.<Type>method() 显式指定。

泛型与注解(Type Annotations)

Java 8(JSR 308)引入了类型注解,允许在任何使用类型的位置添加注解,包括泛型类型参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
import java.util.List;
import java.util.Map;

@Target(ElementType.TYPE_USE)
@interface NonNull {}

@Target(ElementType.TYPE_USE)
@interface Nullable {}

public class TypeAnnotationExample {
// 注解泛型类型参数
private List<@NonNull String> names;

// 注解嵌套泛型
private Map<@NonNull String, @Nullable List<@NonNull Integer>> data;

// 注解通配符边界
private List<@NonNull ? extends Number> numbers;

// 注解数组
private @NonNull String @Nullable [] array;
}

类型注解与 Checker Framework 等静态分析工具配合,可以在编译期检测空指针、线程安全等问题,将运行时错误提前到编译期。

泛型最佳实践

避免原始类型

原始类型(Raw Types)绕过了泛型的类型检查,应始终使用参数化类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.util.ArrayList;
import java.util.List;

public class RawTypeExample {
// 错误:使用原始类型
// List list = new ArrayList();
// list.add("string");
// list.add(42); // 编译通过,运行时可能 ClassCastException

// 正确:使用参数化类型
List<String> typedList = new ArrayList<>();
// typedList.add(42); // 编译错误,类型安全
}

优先使用 List 而非数组

泛型与数组存在根本矛盾(参见"泛型与数组"章节),在泛型上下文中应优先使用集合:

1
2
3
4
5
6
7
8
9
10
import java.util.List;
import java.util.ArrayList;

public class PreferListOverArray {
// 不推荐:泛型数组
// T[] items = new T[10]; // 编译错误

// 推荐:使用 List
// List<T> items = new ArrayList<>();
}

PECS 原则的实践

在设计 API 时,遵循 Producer Extends, Consumer Super 原则:

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.util.Collection;
import java.util.List;
import java.util.ArrayList;

public class PecsExample {
// source 是生产者(读取数据),使用 extends
// destination 是消费者(写入数据),使用 super
public static <T> void copy(List<? extends T> source, List<? super T> destination) {
for (T item : source) {
destination.add(item);
}
}
}

模式速查表

场景关键词 对应模式 方案
类型擦除后方法签名冲突 擦除桥接 编译器自动生成桥方法,反射时用 Method.isBridge() 过滤
需要泛型数组 具化与擦除的冲突 使用 Array.newInstance()List<T> 替代
基类方法返回子类型 类型自约束 <T extends Base<T>> 自限定模式
运行时获取泛型类型 类型令牌 TypeToken / TypeReference 匿名子类捕获
Lambda 类型推断失败 目标类型驱动 类型见证 ClassName.<Type>method()
泛型方法不能重载 擦除签名冲突 使用不同方法名或通配符统一
读取用 extends,写入用 super PECS <? extends T> 生产者,<? super T> 消费者
编译期空值检查 类型注解 @NonNull + Checker Framework

其他资料

angelikalanger 的 JavaGenericsFAQ