本文是笔者系统学习Java泛型的完整笔记,涵盖了从基础语法到底层原理的全部知识点。无论你是初次接触泛型的新手,还是想查漏补缺的老手,相信都能从中受益。
一、背景:为什么需要泛型?
在Java 1.5(2004年)引入泛型之前,我们是这样写代码的:
java
// 没有泛型的时代
public class Box {
private Object content;
public void setContent(Object content) {
this.content = content;
}
public Object getContent() {
return content;
}
}
// 使用时的问题
Box box = new Box();
box.setContent("hello");
// 取出时必须强转,且容易出错
String str = (String) box.getContent();
// 如果误存入其他类型,编译期无法发现
box.setContent(123);
String wrong = (String) box.getContent(); // 运行时ClassCastException!
泛型的核心价值
| 痛点 | 泛型解决方案 |
|---|---|
| 类型不安全 | 编译时类型检查,提前发现错误 |
| 强制转换繁琐 | 消除强制转换,代码更简洁 |
| 代码复用性差 | 类型参数化,一套代码服务多种类型 |
泛型的本质是将"类型"作为参数,让代码在编译时就确定类型,既保证了类型安全,又实现了代码复用。
二、泛型类与泛型接口
2.1 泛型类的定义与使用
java
// 定义泛型类,T是类型参数(Type Parameter)
public class Box<T> {
private T content;
public void setContent(T content) {
this.content = content;
}
public T getContent() {
return content;
}
}
// 使用泛型类
Box<String> stringBox = new Box<>(); // 钻石操作符<>,类型推断
stringBox.setContent("Hello泛型");
String str = stringBox.getContent(); // 无需强转!
// 尝试放入错误类型,编译报错
// stringBox.setContent(123); // ❌ 编译错误
2.2 泛型接口
java
// 泛型接口
public interface Generator<T> {
T generate();
}
// 实现方式1:实现类保留泛型
public class GenericGenerator<T> implements Generator<T> {
private T data;
public GenericGenerator(T data) { this.data = data; }
@Override
public T generate() { return data; }
}
// 实现方式2:实现类指定具体类型
public class StringGenerator implements Generator<String> {
@Override
public String generate() { return "Hello"; }
}
2.3 继承中的泛型规则
子类继承泛型父类时,父类的泛型参数必须"落地":
java
// 父类
class Generic<T> {
private T data;
public T getData() { return data; }
}
// 方式1:子类也保留泛型,类型一致
class ChildGeneric1<T> extends Generic<T> { }
// 方式2:子类明确具体类型
class ChildGeneric2 extends Generic<String> { }
// ❌ 错误写法:子类不泛型,父类类型也不明确
// class ChildGeneric3 extends Generic { } // 编译报错
三、泛型方法
3.1 什么是泛型方法?
泛型方法 是在方法声明时定义了自己的类型参数的方法,与类是否泛型无关。
java
public class Utility {
// 泛型方法:<T>声明类型参数,位于返回值之前
public static <T> T getMiddle(T... args) {
return args[args.length / 2];
}
public static <T> void printArray(T[] array) {
for (T element : array) {
System.out.print(element + " ");
}
}
}
// 使用
String middle = Utility.getMiddle("A", "B", "C"); // T推断为String
Utility.printArray(new Integer[]{1, 2, 3}); // T推断为Integer
3.2 为什么必须写 <T>?
关键问题 :方法的类型参数必须在使用前声明。
java
// ❌ 编译报错:找不到符号 T
public static T getMiddle(T... args) { return args[0]; }
// ✅ 正确:<T>声明了类型参数
public static <T> T getMiddle(T... args) { return args[0]; }
类比:就像使用变量前必须先声明一样,使用类型参数前也必须先声明。
3.3 泛型方法 vs 使用Object
| 对比项 | 泛型方法 | 使用Object |
|---|---|---|
| 类型安全 | ✅ 编译时检查 | ❌ 运行时才暴露 |
| 消除强转 | ✅ | ❌ 需要手动强转 |
| 类型一致性 | ✅ 保证 | ❌ 无法保证 |
java
// 泛型方法:类型安全,无需强转
public static <T> T getFirst(List<T> list) { return list.get(0); }
// 使用Object:需要强转,可能出错
public static Object getFirst(List<?> list) { return list.get(0); }
// 调用:String s = (String) getFirst(list); // 强转,有风险
3.4 泛型类中的静态方法
重要规则:静态方法不能使用类的泛型参数。
java
public class Box<T> {
private T content;
// ✅ 非静态方法可以使用 T
public T getContent() { return content; }
// ❌ 编译报错:静态方法不能使用类的泛型参数
// public static void staticMethod(T t) { }
// ✅ 静态方法可以有自己的类型参数
public static <U> void staticMethod(U u) {
System.out.println(u);
}
}
原因:类的泛型参数在创建对象时确定,而静态方法在类加载时就存在,生命周期不匹配。
四、类型通配符(Wildcard)
4.1 为什么需要通配符?
泛型是不变(invariant) 的:Box<Apple> 和 Box<Fruit> 没有继承关系,即使 Apple extends Fruit。
java
Box<Fruit> fruitBox = new Box<>();
Box<Apple> appleBox = new Box<>();
// ❌ 编译报错:类型不匹配
// fruitBox = appleBox;
// 想让一个方法处理所有水果箱子,就需要通配符
public static void printBox(Box<Fruit> box) { } // 只能接收 Box<Fruit>
public static void printBox(Box<?> box) { } // 可以接收任何 Box
4.2 无界通配符 ?java
java
public static void printBox(Box<?> box) {
System.out.println(box.getContent());
// box.setContent("hello"); // ❌ 不能写入具体类型(除了null)
}
// 可以传入任何类型的Box
printBox(new Box<String>());
printBox(new Box<Integer>());
4.3 上界通配符 ? extends T
java
// Box里装的是Fruit或其子类
public static void printFruit(Box<? extends Fruit> box) {
Fruit f = box.getContent(); // ✅ 读取:一定是Fruit
// box.setContent(new Apple()); // ❌ 不能写入
}
规则 :? extends T → 只能读,不能写(除null)。
4.4 下界通配符 ? super Tjava
java
// Box里装的是Apple或其父类
public static void addApple(Box<? super Apple> box) {
box.setContent(new Apple()); // ✅ 可以写入Apple
// Apple a = box.getContent(); // ❌ 读取只能是Object
}
规则 :? super T → 只能写,读出来是Object。
4.5 PECS原则(终极口诀)
Producer Extends, Consumer Super
| 场景 | 推荐写法 | 原因 |
|---|---|---|
| 生产者:从容器中读取数据 | ? extends T |
保证读取到的一定是T或其子类 |
| 消费者:向容器中写入数据 | ? super T |
保证能写入T及其子类 |
| 既读又写 | 不要用通配符,用 <T> |
需要保持类型一致性 |
4.6 T vs ? 的区别
| 对比 | T(类型参数) |
?(通配符) |
|---|---|---|
| 本质 | 声明一个类型变量 | 使用一个未知类型 |
| 能否多次使用 | ✅ 可以 | ❌ 每次独立 |
| 作用范围 | 整个类或方法 | 仅当前声明位置 |
| 能否写入 | ✅ 可以 | 受限(取决于上下界) |
java
// T:多次使用,保持类型一致
public static <T> void swap(Box<T> box1, Box<T> box2) {
T temp = box1.getContent();
box1.setContent(box2.getContent());
box2.setContent(temp);
}
// ?:每次独立,不能多次引用
public static void process(Box<?> box1, Box<?> box2) {
// ? temp = box1.getContent(); // ❌ 编译错误
}
五、类型擦除(Type Erasure)
5.1 什么是类型擦除?
核心定义 :Java泛型是编译时 的概念,在运行时,泛型类型信息会被编译器擦除,替换为原始类型。
java
// 编译时
List<String> stringList = new ArrayList<>();
List<Integer> integerList = new ArrayList<>();
// 运行时(字节码)
List stringList = new ArrayList();
List integerList = new ArrayList();
// 两者在运行时完全相同!
5.2 为什么设计成擦除式?
为了向后兼容:Java 1.5才引入泛型,为了能让旧代码无缝迁移,必须让新代码在运行时和旧代码保持一致。
5.3 擦除的具体规则
| 擦除规则 | 示例 |
|---|---|
| 无界类型 → Object | T → Object |
| 有界类型 → 第一个边界 | T extends Number → Number |
| 多个边界 → 第一个边界 | T extends Number & Comparable → Number |




5.4 擦除带来的限制
| 限制 | 说明 | 变通方案 |
|---|---|---|
不能用instanceof检查泛型类型 |
list instanceof List<String> 编译报错 |
只检查原始类型 |
不能new T() |
运行时T被擦除为Object | 传入Class<T>并反射创建 |
不能new T[] |
运行时无法确定数组类型 | 使用Array.newInstance() |
| 静态上下文不能使用泛型参数 | 生命周期不匹配 | 使用泛型方法 |
5.5 补偿机制:桥接方法
为了保持多态,编译器会自动生成桥接方法(Bridge Method):
java
// 源码
class MyList implements List<String> {
public void add(String item) { }
}
// 擦除后,编译器生成桥接方法
class MyList implements List {
public void add(String item) { } // 用户方法
public void add(Object item) { // 桥接方法(自动生成)
add((String) item);
}
}
六、泛型和数组
6.1 核心矛盾
数组在运行时必须知道具体类型,泛型在运行时类型被擦除。 → 两者天然冲突。
java
// ✅ 数组:运行时知道类型
String[] strArray = new String[10];
Integer[] intArray = new Integer[10];
// strArray.getClass() → String[]
// intArray.getClass() → Integer[]
// ❌ 泛型:运行时丢失类型
List<String> strList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
// strList.getClass() → ArrayList
// intList.getClass() → ArrayList
6.2 规则:可以声明,不能直接创建java
java
// ✅ 可以声明带泛型的数组引用
Box<String>[] boxArray;
// ❌ 不能直接创建带泛型的数组
// boxArray = new Box<String>[10]; // 编译报错
// ✅ 可以通过反射创建
boxArray = (Box<String>[]) new Box[10]; // 有警告,但可行
// ✅ 最佳实践:使用 Array.newInstance()
public static <T> T[] createArray(Class<T> clazz, int size) {
@SuppressWarnings("unchecked")
T[] array = (T[]) Array.newInstance(clazz, size);
return array;
}
6.3 为什么Array.newInstance()可以?
因为通过Class<T>对象在运行时获取了具体类型,绕过了泛型擦除的限制。
七、最佳实践与速查表
7.1 泛型使用决策表
| 场景 | 推荐写法 | 示例 |
|---|---|---|
| 定义一个容器类 | 泛型类 | class Box<T> |
| 方法的返回值和参数需要类型一致 | 泛型方法 | <T> T getFirst(List<T>) |
| 只需要读取数据 | ? extends T |
List<? extends Number> |
| 只需要写入数据 | ? super T |
List<? super Integer> |
| 既要读又要写 | 不用通配符,用<T> |
void swap(Box<T>, Box<T>) |
| 完全不关心类型 | ? |
List<?> |
7.2 常见泛型符号
| 符号 | 含义 | 常用场景 |
|---|---|---|
T |
Type(类型) | 通用类型参数 |
E |
Element(元素) | 集合元素类型 |
K |
Key(键) | Map的键 |
V |
Value(值) | Map的值 |
R |
Result(结果) | 返回结果类型 |
? |
未知类型 | 通配符 |
7.3 重要规则速记
-
静态上下文不能使用类的泛型参数
-
不能创建泛型数组 (
new T[10]不允许) -
不能
instanceof泛型类型 -
泛型类型在运行时被擦除
-
PECS原则 :Producer
extends,Consumersuper -
子类继承父类时,父类的泛型必须"落地"
-
**钻石操作符
<>**让类型推断更简洁
八、结语
泛型是Java语言中一个精妙而强大的设计。它通过编译时类型检查 保证了类型安全,通过类型擦除 实现了向后兼容,通过通配符提供了灵活的类型约束。
理解泛型的核心在于把握两个"对立统一":
-
类型参数(
T) 与类型通配符(?):一个用于定义和保持一致性,一个用于使用和增加灵活性 -
编译时安全 与运行时擦除:编译时严格的类型检查,运行时零开销的兼容性
掌握泛型,你的Java代码将更加安全、优雅和通用。