前言
在上一篇文章《String、StringBuilder、StringBuffer深度剖析》中,我们深入学习了String家族的底层原理。但在日常开发中,还有一个特性我们每天都在用,却很少深究其原理------泛型。
List<String>和List<Integer>在运行时是同一个类型吗?为什么泛型参数不能用基本类型?<? extends T>和<? super T>到底有什么区别?这些问题背后,都指向同一个核心概念------类型擦除。
今天,我们就来彻底揭开泛型的神秘面纱。读完本文,你将能回答:
- 什么是类型擦除?编译后的字节码长什么样?
- 为什么会有桥方法?
- PECS原则是什么?如何正确使用通配符?
- 泛型与反射的局限性是什么?
下一篇,我们将进入反射与动态代理------Java语言动态性的核心。
一、泛型基础回顾
1.1 什么是泛型?
泛型是JDK 5引入的特性,允许在定义类、接口、方法时使用类型参数,实现代码的复用和类型安全。
java
// 没有泛型(JDK 5之前)
List list = new ArrayList();
list.add("hello");
list.add(123); // 可以混入不同类型
String s = (String) list.get(0); // 需要强制转换
// 有泛型(JDK 5+)
List<String> list = new ArrayList<String>();
list.add("hello");
// list.add(123); // 编译错误!类型安全
String s = list.get(0); // 无需强制转换
1.2 泛型的三种使用方式
| 方式 | 示例 | 说明 |
|---|---|---|
| 泛型类 | class Box<T> { private T item; } |
整个类使用类型参数 |
| 泛型接口 | interface List<T> { void add(T item); } |
接口定义类型参数 |
| 泛型方法 | public <T> T getValue(T t) { return t; } |
方法级别定义类型参数 |
java
// 泛型类
public class Box<T> {
private T content;
public void set(T content) { this.content = content; }
public T get() { return content; }
}
// 泛型方法(与泛型类无关)
public class Util {
public static <T> T getMiddle(T... arr) {
return arr[arr.length / 2];
}
}
// 使用
Box<String> stringBox = new Box<>();
stringBox.set("hello");
String s = stringBox.get();
Integer i = Util.getMiddle(1, 2, 3); // 类型推断
二、类型擦除(Type Erasure)
2.1 什么是类型擦除?
类型擦除是Java泛型的核心实现机制:编译期间,泛型信息会被移除(擦除),替换为原生类型(Raw Type),并插入必要的强制转换。
java
// 源码
public class Box<T> {
private T content;
public void set(T content) {
this.content = content;
}
public T get() {
return content;
}
}
// 编译后(字节码等价代码)
public class Box {
private Object content; // T被擦除为Object
public void set(Object content) {
this.content = content;
}
public Object get() {
return content;
}
}
2.2 字节码验证
使用javap -c Box.class查看字节码:
public class Box {
private java.lang.Object content; // 擦除后变成Object
public void set(java.lang.Object);
Code:
0: aload_0
1: aload_1
2: putfield #2 // Field content:Ljava/lang/Object;
5: return
public java.lang.Object get();
Code:
0: aload_0
1: getfield #2 // Field content:Ljava/lang/Object;
4: areturn
}
2.3 有界类型参数的擦除
如果泛型参数指定了上界,擦除时会用第一个上界替换:
java
// 源码
public class NumberBox<T extends Number & Comparable<T>> {
private T content;
public void set(T content) {
this.content = content;
}
public T get() {
return content;
}
}
// 擦除后(用第一个上界Number替换)
public class NumberBox {
private Number content; // T被擦除为Number
public void set(Number content) {
this.content = content;
}
public Number get() {
return content;
}
}
2.4 类型擦除的后果
| 后果 | 说明 | 示例 |
|---|---|---|
| 运行时类型信息丢失 | List<String>和List<Integer>运行时相同 |
list instanceof List<String> 编译错误 |
| 泛型参数不能用基本类型 | 擦除后需要Object,基本类型不兼容 | List<int> 编译错误 |
| 不能创建泛型数组 | 数组需要知道确切类型 | new T[10] 编译错误 |
| 静态上下文不能使用类型参数 | 静态成员属于类,与实例类型参数无关 | static T value 编译错误 |
java
// 运行时类型信息丢失
List<String> stringList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
System.out.println(stringList.getClass() == intList.getClass()); // true,都是ArrayList
// 不能创建泛型数组
// T[] arr = new T[10]; // 编译错误
// 静态上下文不能使用类型参数
public class Box<T> {
// private static T value; // 编译错误
// public static T getValue() { return value; } // 编译错误
}
三、桥方法(Bridge Method)
3.1 为什么需要桥方法?
当子类重写父类的泛型方法时,由于类型擦除,父类方法签名变为Object参数,而子类可能是具体类型,导致方法签名不匹配。编译器会生成桥方法来维持多态。
3.2 桥方法示例
java
// 父类
public class Parent<T> {
public void set(T value) {
System.out.println("Parent.set: " + value);
}
}
// 子类
public class Child extends Parent<String> {
@Override
public void set(String value) {
System.out.println("Child.set: " + value);
}
}
编译后发生了什么?
java
// 擦除后的Parent
public class Parent {
public void set(Object value) {
System.out.println("Parent.set: " + value);
}
}
// 擦除后的Child
public class Child extends Parent {
// 子类自己的方法
public void set(String value) {
System.out.println("Child.set: " + value);
}
// 编译器生成的桥方法!
public void set(Object value) {
set((String) value); // 强制转换后调用子类方法
}
}
3.3 字节码验证
使用javap -c Child.class查看:
public class Child extends Parent {
public void set(java.lang.String);
Code:
0: getstatic #2 // Field java/lang/System.out
3: new #3 // class StringBuilder
6: dup
7: invokespecial #4 // StringBuilder."<init>":()V
10: ldc #5 // String "Child.set: "
12: invokevirtual #6 // StringBuilder.append
15: aload_1
16: invokevirtual #6 // StringBuilder.append
19: invokevirtual #7 // StringBuilder.toString
22: invokevirtual #8 // PrintStream.println
25: return
// 桥方法(Bridge Method)
public void set(java.lang.Object);
Code:
0: aload_0
1: aload_1
2: checkcast #9 // class java/lang/String
5: invokevirtual #10 // Method set:(Ljava/lang/String;)V
8: return
}
3.4 桥方法的识别
java
// 通过反射识别桥方法
Method[] methods = Child.class.getDeclaredMethods();
for (Method m : methods) {
System.out.println(m.getName() + " - bridge: " + m.isBridge());
}
// 输出:
// set - bridge: false (子类自己的方法)
// set - bridge: true (桥方法)
四、通配符(Wildcard)
4.1 为什么需要通配符?
由于泛型是不可变的(List<String>不是List<Object>的子类型),通配符提供了协变和逆变的能力。
java
// 泛型是不可变的
List<String> strings = new ArrayList<>();
// List<Object> objects = strings; // 编译错误!
// 使用通配符
List<? extends Object> objects = strings; // 可以
4.2 三种通配符
| 通配符 | 语法 | 说明 | 读/写 |
|---|---|---|---|
| 无界通配符 | <?> |
未知类型 | 只能读(读为Object),不能写(null除外) |
| 上界通配符 | <? extends T> |
T或T的子类 | 只能读(读为T),不能写 |
| 下界通配符 | <? super T> |
T或T的父类 | 可以写(写入T及其子类),读只能读为Object |
4.3 上界通配符:<? extends T>
java
public void processNumbers(List<? extends Number> list) {
// 读取:可以,Number是所有元素的父类型
Number n = list.get(0);
// 写入:不可以!因为不知道具体类型
// list.add(123); // 编译错误
// list.add(new Integer(123)); // 编译错误
list.add(null); // 只有null可以
}
// 使用
List<Integer> ints = Arrays.asList(1, 2, 3);
List<Double> doubles = Arrays.asList(1.1, 2.2, 3.3);
processNumbers(ints); // OK
processNumbers(doubles); // OK
为什么不能写入? 因为List<? extends Number>可能是List<Integer>、List<Double>等,写入Number的任何子类型都可能造成类型错误。
4.4 下界通配符:<? super T>
java
public void addNumbers(List<? super Integer> list) {
// 写入:可以,Integer是Integer或其父类
list.add(123);
list.add(456);
// 读取:只能读为Object
Object obj = list.get(0);
// Integer i = list.get(0); // 编译错误
}
// 使用
List<Number> numbers = new ArrayList<>();
List<Object> objects = new ArrayList<>();
addNumbers(numbers); // OK
addNumbers(objects); // OK
// List<Integer> ints = new ArrayList<>();
// addNumbers(ints); // 编译错误,Integer不是Integer的父类
为什么只能读为Object? 因为List<? super Integer>可能是List<Integer>、List<Number>、List<Object>,读取时只能确保是Object类型。
4.5 PECS原则(Producer Extends, Consumer Super)
这是Joshua Bloch在《Effective Java》中提出的经典原则:
| 角色 | 通配符 | 说明 |
|---|---|---|
| Producer(生产者) | <? extends T> |
只读不写,使用extends |
| Consumer(消费者) | <? super T> |
只写不读,使用super |
java
// 生产者:从集合中读取数据
public void copy(List<? extends Number> source, List<? super Number> dest) {
// source是生产者,使用extends
// dest是消费者,使用super
for (Number n : source) {
dest.add(n);
}
}
// 使用
List<Integer> source = Arrays.asList(1, 2, 3);
List<Number> dest = new ArrayList<>();
copy(source, dest); // OK
记忆口诀 :PECS = Producer Extends, Consumer Super
五、泛型与反射
5.1 泛型信息在运行时的残留
虽然类型擦除了,但泛型信息部分保留在字节码中(Signature属性),反射可以获取。
java
public class GenericTypeDemo {
private List<String> stringList;
private Map<String, Integer> map;
public static void main(String[] args) throws Exception {
Field field = GenericTypeDemo.class.getDeclaredField("stringList");
// 获取泛型类型
Type type = field.getGenericType();
if (type instanceof ParameterizedType) {
ParameterizedType pt = (ParameterizedType) type;
System.out.println("Raw type: " + pt.getRawType()); // interface java.util.List
System.out.println("Actual type: " + pt.getActualTypeArguments()[0]); // class java.lang.String
}
}
}
// 输出:
// Raw type: interface java.util.List
// Actual type: class java.lang.String
5.2 获取方法返回值泛型
java
public class MethodGenericDemo {
public List<String> getNames() {
return Arrays.asList("Alice", "Bob");
}
public static void main(String[] args) throws Exception {
Method method = MethodGenericDemo.class.getMethod("getNames");
Type returnType = method.getGenericReturnType();
if (returnType instanceof ParameterizedType) {
ParameterizedType pt = (ParameterizedType) returnType;
System.out.println("Return type: " + pt.getRawType()); // interface java.util.List
System.out.println("Parameter: " + pt.getActualTypeArguments()[0]); // class java.lang.String
}
}
}
5.3 反射绕开泛型检查
java
// 通过反射可以绕过编译期的泛型检查
List<Integer> intList = new ArrayList<>();
intList.add(123);
// 通过反射添加String
Method method = intList.getClass().getMethod("add", Object.class);
method.invoke(intList, "hello");
System.out.println(intList.get(0)); // 123
System.out.println(intList.get(1)); // hello
// 但运行时没有类型错误,因为擦除后都是Object
六、常见面试题
Q1:Java泛型是编译时还是运行时机制?
答 :Java泛型主要是编译时机制。编译期间会进行类型擦除,运行时泛型信息大部分丢失。但通过反射可以获取部分泛型信息(Signature属性)。
Q2:List<String>和List<Integer>在运行时是否相同?
答 :运行时相同,都是List类型(或ArrayList)。因为类型擦除后,泛型参数被移除。
java
System.out.println(new ArrayList<String>().getClass() == new ArrayList<Integer>().getClass()); // true
Q3:为什么不能创建泛型数组?
答 :因为数组是协变 的(String[]是Object[]的子类型),且数组在运行时知道其元素类型。如果允许创建泛型数组,类型擦除会导致运行时类型检查失败。
java
// 假设允许这样写
// T[] arr = new T[10];
// 擦除后变成
Object[] arr = new Object[10];
// 但这样赋值就会有问题
String[] strArr = (String[]) arr; // 运行时ClassCastException
Q4:<? extends T>和<? super T>有什么区别?
答:
<? extends T>:T或T的子类(上界),作为生产者(读取),不能写入<? super T>:T或T的父类(下界),作为消费者(写入),读取只能读为Object
Q5:什么是PECS原则?
答 :PECS = Producer Extends, Consumer Super。如果需要从集合中读取数据(生产),使用<? extends T>;如果需要向集合中写入数据(消费),使用<? super T>。
Q6:桥方法是什么?为什么需要?
答 :桥方法是编译器自动生成的方法,用于维持多态性。当子类重写父类的泛型方法时,由于类型擦除,方法签名不匹配(父类是set(Object),子类是set(String)),桥方法通过强制转换调用子类方法,确保多态正常工作。
七、总结
7.1 核心要点
| 概念 | 一句话解释 |
|---|---|
| 类型擦除 | 编译期间泛型信息被移除,替换为原生类型 |
| 桥方法 | 编译器生成的方法,维持泛型多态 |
无界通配符 <?> |
未知类型,只能读为Object |
上界通配符 <? extends T> |
T或子类,只能读,不能写 |
下界通配符 <? super T> |
T或父类,能写,读只能为Object |
| PECS | Producer Extends, Consumer Super |
7.2 类型擦除的影响
┌─────────────────────────────────────────────────────────────────────┐
│ 类型擦除的影响 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ✅ 可以做的: │
│ ├─ 泛型类、泛型接口、泛型方法 │
│ ├─ 类型安全的集合操作 │
│ ├─ 通过反射获取泛型签名 │
│ └─ 通配符实现协变和逆变 │
│ │
│ ❌ 不能做的: │
│ ├─ 运行时判断泛型类型(`instanceof List<String>`) │
│ ├─ 创建泛型数组(`new T[10]`) │
│ ├─ 基本类型作为泛型参数(`List<int>`) │
│ └─ 静态上下文使用类型参数(`static T value`) │
│ │
└─────────────────────────────────────────────────────────────────────┘
7.3 面试金句
如果面试官问你"Java泛型的原理",你可以这样回答:
"Java泛型是通过类型擦除 实现的,属于编译时机制。编译期间,泛型信息被擦除,替换为原生类型(如
T擦除为Object),并插入必要的强制转换。为了维持多态,编译器会生成桥方法 ,例如子类重写父类的泛型方法时,桥方法负责将Object参数强制转换后调用子类的具体方法。由于类型擦除,运行时List<String>和List<Integer>是同一个类。为了弥补类型擦除带来的灵活性损失,Java提供了通配符机制:<? extends T>用于生产者(只读),<? super T>用于消费者(只写),这就是PECS原则。"
下篇预告
理解了泛型的底层原理,我们掌握了Java类型系统的核心特性。但Java还有一种在运行时操作类型的能力------反射。
反射是如何获取类的信息的?动态代理是如何实现的?Spring和MyBatis是如何利用反射的?
下一篇《反射与动态代理------Java语言动态性的核心》将带你深入反射的底层实现,揭开动态代理的神秘面纱。
如果你觉得本文有帮助,欢迎点赞、评论、转发!