一、概述
Java 中的泛型是一种比较常用且强大的功能,它是一种代码类型检验机制,使用泛型可以让你在代码编译期间检测到更多察觉不到的错误。
二、优点与缺点
2.1 优点
- 在编译时进行更强的类型检测,Java 编译器将强类型检查应用于通用代码里,如果有违反类型安全,则直接通过编译器暴露出来。
- 消除类型转换:使用泛型可以免去强制转型。
- 使用泛型,可以实现不同类型的集合进行工作,可以实现自定义并且类型安全且易于阅读的泛型算法。
2.2 缺点(因类型擦除导致)
- 泛型类型变量不能使用基本数据类型(必须用包装类型)
- 不能使用 instanceof 运算符检测泛型类型
- 泛型类不能被定义成静态的(静态成员无法引用类类型参数)
- 泛型类里同名函数会造成方法冲突
- 无法创建泛型实例(new T() 无效,可以通过反射实现)
- 不存在泛型数组(new T[n] 无效)
三、泛型的分类
3.1 通用类型(Generic Type)
通用类型是通过类型进行参数化的通用类或接口。
通用类的定义格式如下:
class name<T1, T2, ..., Tn> {} |
类型参数命名约定:
- E - Element(Java Collections Framework 广泛使用)
- K - Key
- V - Value
- N - Number
- T - Type
- S, U, V etc. - 2nd, 3rd, 4th types
调用和实例化泛型类型:泛型类型的调用通常称为参数化类型,要实例化此类,通常是使用 new 关键字:
List<String> strList = new ArrayList<>(); |
多类型参数:
public interface Pair<K, V> { |
通过以下语句创建 OrderedPair 的两个实例:
Pair<String, Integer> p1 = new OrderedPair<String, Integer>("Even", 8); |
由于菱形原因,Java 编译器可以从上下文推断出类型参数,因此可以使用菱形表示法缩短实例化语句:
Pair<String, Integer> p1 = new OrderedPair<>("Even", 8); |
参数化类型:
OrderedPair<String, Box<Integer>> p = new OrderedPair<>("primes", new Box<Integer>(...)); |
3.2 原始类型(Raw Type)
原始类型是没有任何类型参数的泛型类或接口的名称。例如:
public class Box<T> { |
但非泛型类或接口类型不是原始类型。原始类型显示在旧代码中,因为在 JDK 5.0 之前,许多 API 类(例如 Collections 类)不是通用类型。使用原始类型时,实际上也会获得泛型行为(Box 内部存储 Object)。为了向后兼容,允许将参数化类型分配给其他原始类型:
Box<String> stringBox = new Box<>(); |
但反过来,将原始类型分配给参数化类型,就会提示警告:
Box rawBox = new Box(); // 原始类型 |
同样,如果使用原始类型来调用相应泛型类型中定义的泛型方法,也会提示警告:
Box<String> stringBox = new Box<>(); |
该警告表明原始类型会绕过通用类型检查,从而将不安全代码的捕获推迟到运行时。因此,应避免使用原始类型。
3.3 通用方法(Generic Method)
指引入自己的类型参数的方法,类似于声明一个泛型方法,但类型参数的范围仅限于声明它的方法。允许使用静态和非静态的泛型方法,也允许使用泛型类构造函数。
通用方法的语法包括:类型参数列表,在尖括号内,该列表出现在方法的返回类型之前。对于静态泛型方法,类型参数部分必须出现在方法的返回类型之前:
public class Util { |
调用此方法的完整语法如下:
Pair<Integer, String> p1 = new Pair<>(1, "apple"); |
该类型可以明确提供,如上所示,通常可以忽略,编译器可推断出所需类型:
boolean same = Util.compare(p1, p2); |
3.4 限定类型参数(Bounded Type Parameter)
限制参数化类型中用作类型参数的类型,比如对数字进行操作的方法只希望接受 Number 或其子类的实例:
public class Box<T> { |
3.5 泛型、继承和子类型
通过泛型编程时,有一种常见的误解:Integer 是 Number 类型的子类,那么它的泛型包裹类 Box<Integer> 和 Box<Number> 没有任何关系——它们是不同的类型,不能相互赋值。
// 这是错误的认知: |
可以通过扩展来实现泛型通用类或接口。一个类或者接口的类型参数与另一个类或接口的类型参数之间的关系由 extends 和 implements 来确定。
四、类型推断
类型推断是 Java 编译器查看每个方法调用和相应声明以确定使用适用的类型参数的能力。
- 类型推断和通用方法:通常 Java 编译器可以推断出通用方法调用的类型参数。
- 泛型类的类型推断和实例化:可以使用一组空的类型参数(<>)替换调用通用类的构造函数所需的类型参数,只要编译器可以从上下文中推断出类型参数都可以使用。
- 泛型和非泛型类的类型推断和泛型构造函数:
class MyClass<X> { |
实例化时:
new MyClass<Integer>(""); // X=Integer, T=String |
4.1 Java 8/11/17 中的类型推断增强
// Java 8: 目标类型推断改进 |
五、通配符
在通用代码里,通配符(?)表示未知类型。通配符可以在多种情况下使用:作为参数、字段或局部变量的类型;作为返回类型。请注意:通配符从不用作泛型方法调用、泛型类实例创建、超类型的类型参数。
5.1 上限通配符
声明上限通配符,使用通配符(?),后跟 extends 关键字,然后是其上限:
public static void process(List<? extends Foo> list) { |
5.2 下限通配符
下限通配符使用通配符(?)表示,后跟 super 关键字,接着再跟下限:
public static void addNumbers(List<? super Integer> list) { |
5.3 无限通配符
无限通配符类型使用通配符(?)来指定:
// 适用于: |
5.4 PECS 原则(Producer Extends, Consumer Super)
PECS 是通配符使用的黄金法则:
- Producer Extends:如果你需要从集合中读取(产生数据),使用
? extends Type - Consumer Super:如果你需要向集合中写入(消费数据),使用
? super Type
// 经典示例:Collections.copy |
六、类型擦除(Type Erasure)
6.1 类型擦除原理
Java 的泛型是 JDK 5 新引入的特性,为了向下兼容,虚拟机其实是不支持泛型的,所以 Java 实现的是一种伪泛型机制——也就是说 Java 在编译期擦除了所有的泛型信息,这样 Java 就不需要产生新的类型到字节码,所有的泛型类型最终都是一种原始类型,在 Java 运行时根本就不存在泛型信息。
Java 编译器具体是如何擦除泛型的:
- 检查泛型类型,获取目标类型
- 擦除类型变量,并替换为限定类型
- 如果泛型类型的类型变量没有限定(
<T>),则用 Object 作为原始类型 - 如果有限定(
<T extends XClass>),则用 XClass 作为原始类型 - 如果有多个限定(
<T extends XClass1 & XClass2>),则使用第一个边界 XClass1 作为原始类型
- 如果泛型类型的类型变量没有限定(
- 在必要时插入类型转换以保持类型安全
- 生成桥方法以在扩展时保持多态性
// 类型擦除的实际效果 |
6.2 桥方法(Bridge Method)
桥方法是类型擦除的一个重要副作用。当子类在继承泛型父类时指定了具体类型参数,编译器会自动生成桥方法来保持多态性:
// 编译前 |
桥方法在反射中可见——当你用 getDeclaredMethods() 时,会看到两个同名方法(一个返回 Object,一个返回具体类型)。可以通过 Method.isBridge() 判断是否为桥方法。
6.3 类型擦除的后果
// 1. 不能实例化泛型类型 |
七、Gson 泛型反序列化与 TypeToken
Gson 在处理泛型反序列化时需要保留泛型信息,因为运行时泛型已被擦除:
// 问题:如何反序列化泛型类型? |
为什么 TypeToken 必须是抽象类(或接口)?
因为只有定义为抽象类或接口,才能在使用时创建匿名子类(new TypeToken<Response<Data>>() {}),这个匿名子类会保留父类的泛型签名信息。JVM 规范要求类的字节码中记录其父类的泛型信息——即使方法体中的泛型被擦除了,类声明中的泛型信息仍然保留在常量池的 Signature 属性中。TypeToken 通过 getClass().getGenericSuperclass() 获取 ParameterizedType,从中提取出实际的类型参数。
// TypeToken 核心原理(简化版) |
八、Kotlin 的泛型 vs Java 的泛型
| 特性 | Java | Kotlin |
|---|---|---|
| 类型擦除 | 是(运行时无泛型) | 是(JVM 后端) |
| 声明处型变 | 不支持 | 支持(out/in 关键字) |
| 使用处型变 | 支持(? extends/? super) |
支持(型变投影) |
| reified 类型参数 | 不支持 | 支持(内联函数中 reified T) |
| 星投影 | 不支持 | 支持(*) |
| 基本类型泛型 | 不支持(需要装箱) | 编译期特化处理 |
// Kotlin 声明处型变 |
九、小结
- Java 泛型(generics)是 JDK 5 中引入的一种参数化类型特性
- 使用泛型好处:代码更健壮、代码更简洁、代码更灵活、可复用性高
- 类型擦除是 Java 泛型的核心实现机制,理解它才能理解泛型的各种限制
- PECS 原则是正确使用通配符的关键指南
十、面试常问题目
Q1: 什么是类型擦除?为什么 Java 选择这种实现方式?
类型擦除是 Java 泛型的实现机制——编译器在编译期间将泛型类型参数替换为它们的边界(未限定的则替换为 Object),并在需要时插入强制类型转换。这样做主要是为了向后兼容:JDK 5 之前的大量代码(包括 JDK 自身的集合框架)没有泛型,类型擦除使得泛型化的新代码可以与老代码二进制兼容。代价是运行时无法获取泛型类型信息(无法 T.class、无法 new T()、无法 instanceof List<String>)。
Q2: <? extends T> 和 <? super T> 有什么区别?什么场景下分别使用?
<? extends T> 是上限通配符,表示类型是 T 或其子类。适用于”生产者”角色——你只从集合中读取数据。因为不能确定具体类型,所以不能写入(除 null)。典型场景:List<? extends Number> 可以传入 List<Integer> 或 List<Double>,从其中读取的值类型为 Number。<? super T> 是下限通配符,表示类型是 T 或其父类。适用于”消费者”角色——你只向集合中写入 T 类型的数据。因为从集合中读取的类型不确定,只能以 Object 方式读取。典型场景:List<? super Integer> 可以传入 List<Integer>、List<Number> 或 List<Object>,可以向其中安全添加 Integer。PECS 原则:Producer Extends, Consumer Super。
Q3: 桥方法是什么?它在什么情况下产生?
桥方法是编译器为了保持泛型方法多态性而自动生成的方法。当子类继承(或实现)泛型父类(或接口)并指定了具体类型参数时,类型擦除后,子类方法的签名与父类签名不匹配(父类期望 Object 参数,子类定义了 String 参数)。编译器在子类中生成一个桥接方法(签名为父类的擦除后签名),内部调用子类具体类型的方法。Method.isBridge() 可以判断一个方法是否为桥方法。桥方法在字节码层面可见,可能导致调用 getDeclaredMethods() 时看到”重复”方法。
Q4: 为什么 Gson 反序列化泛型类型需要 TypeToken?
因为 Java 泛型在运行时被擦除,Response.class 不包含 Response<Data> 中 Data 的类型信息。如果直接使用 Response.class 进行反序列化,Gson 不知道 data 字段应该是什么类型,会退化为使用 LinkedTreeMap。TypeToken 通过创建匿名子类(new TypeToken<Response<Data>>() {}),将泛型信息保存在类的 Signature 属性中(属性在字节码的常量池中,不会被擦除)。然后通过反射获取 ParameterizedType,从中提取出实际的类型参数 Data。
Q5: 为什么不能创建泛型数组?
因为 Java 的泛型是通过类型擦除实现的,运行时不保留类型信息。如果允许创建泛型数组(如 new List<String>[10]),那么运行时数组的实际类型是 List[](擦除后),但元素类型丢失。这会导致数组的协变特性(Object[] arr = new List<String>[10] 是合法的)加上泛型擦除后,可以在运行时向数组中放入 List<Integer> 而不会抛出 ArrayStoreException——这就打破了泛型的类型安全承诺。Java 设计者选择在编译期禁止泛型数组创建(但在一些场景下可以通过 (T[]) new Object[n] 绕过,此时开发者和编译器共同承担类型安全责任)。
参考文档:
- Java Language Specification, Chapter 4 (Types, Values, and Variables) and Chapter 18 (Type Inference)
- Angelika Langer’s Java Generics FAQ:
http://www.angelikalanger.com/GenericsFAQ/JavaGenericsFAQ.html - Gson TypeToken 源码:
https://github.com/google/gson - Kotlin Generics:
https://kotlinlang.org/docs/generics.html

