泛型详解

核心问题:如何让一个类同时支持多种类型?

场景引入

// 需求:设计一个"盒子"类,能存储不同类型的数据
// 问题:如何实现?

// 方案1:为每种类型写一个类(❌ 类爆炸)
class IntegerBox { private Integer value; }
class StringBox { private String value; }
class UserBox { private User value; }
// ...无穷无尽

// 方案2:使用Object(❌ 类型不安全)
class Box {
    private Object value;
    public void set(Object value) { this.value = value; }
    public Object get() { return value; }
}
Box box = new Box();
box.set("hello");
Integer i = (Integer) box.get();  // 💥 ClassCastException

// 方案3:泛型(✅ 类型安全 + 代码复用)
class Box<T> {
    private T value;
    public void set(T value) { this.value = value; }
    public T get() { return value; }
}
Box<String> box = new Box<>();
box.set("hello");
String s = box.get();  // 无需强转,编译期保证类型安全

一、为什么需要泛型?(JDK 5之前的痛点)

1.1 JDK 5之前的写法

// 没有泛型的集合
List list = new ArrayList();
list.add("hello");
list.add(123);           // ✅ 编译通过
list.add(new User());    // ✅ 编译通过

// 取出时必须强转
String s = (String) list.get(0);   // ✅ 正常
String s2 = (String) list.get(1);  // 💥 运行时 ClassCastException

1.2 三大核心问题

问题 具体表现 后果
类型不安全 任何类型都能放入集合 运行时才发现类型错误
强制转换繁琐 每次取值都需要 (Type) 代码冗长,容易出错
可读性差 看不出集合存储的类型 维护困难

1.3 泛型解决方案

List<String> list = new ArrayList<>();
list.add("hello");       // ✅ 正常
list.add(123);           // ❌ 编译错误!类型不匹配
list.add(new User());    // ❌ 编译错误!

String s = list.get(0);  // ✅ 无需强转,编译器保证类型

泛型的四大好处:

  1. 编译期类型检查 → 错误提前发现
  2. 避免强制转换 → 代码更简洁(实际是编译器自动插入了强转)
  3. 提升可读性List<String> 一看就知道存String
  4. 代码复用 → 一个类适配所有类型

二、泛型的基本使用

2.1 泛型类

public class Box<T> {
    private T value;

    public void set(T value) {
        this.value = value;
    }

    public T get() {
        return value;
    }
}

// 使用
Box<String> stringBox = new Box<>();
stringBox.set("hello");
String s = stringBox.get();

Box<Integer> intBox = new Box<>();
intBox.set(100);
Integer i = intBox.get();

2.2 泛型接口

public interface Repository<T> {
    void save(T entity);
    T findById(Long id);
    List<T> findAll();
}

// 实现时指定具体类型
public class UserRepository implements Repository<User> {
    @Override
    public void save(User entity) { }

    @Override
    public User findById(Long id) { return null; }

    @Override
    public List<User> findAll() { return null; }
}

2.3 泛型方法

public class Utils {
    // 静态泛型方法(必须自己声明类型参数)
    public static <T> T getFirst(List<T> list) {
        return list.isEmpty() ? null : list.get(0);
    }

    // 多个类型参数
    public static <K, V> Map<K, V> createMap() {
        return new HashMap<>();
    }

    // 有界类型参数
    public static <T extends Number> double sum(List<T> list) {
        double total = 0;
        for (T num : list) {
            total += num.doubleValue();  // Number的方法
        }
        return total;
    }
}

三、类型擦除(Type Erasure)⭐⭐⭐

3.1 什么是类型擦除?

核心定义: Java泛型是"伪泛型",编译后所有泛型信息都会被擦除,字节码中只剩下原始类型(Raw Type)。

List<String> stringList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();

// 运行时,两者的Class完全相同!
System.out.println(stringList.getClass() == intList.getClass());
// true!都是 ArrayList.class

反射验证:泛型只在编译期生效

List<String> list = new ArrayList<>();
list.add("hello");

// 通过反射可以绕过泛型检查
Method add = ArrayList.class.getMethod("add", Object.class);
add.invoke(list, 100);  // ✅ 成功添加Integer!

System.out.println(list);  // [hello, 100]
String s = list.get(1);    // 💥 运行时ClassCastException

3.2 为什么要类型擦除?

历史原因:向后兼容

Java 1.42002年) → 没有泛型,代码已经大量部署
        ↓
Java 52004年) → 引入泛型,如何兼容旧代码?
        ↓
解决方案:编译期检查泛型,运行时擦除泛型信息
        ↓
结果:新代码(有泛型)和旧代码(无泛型)可以无缝共存

设计思路推演:

  1. ❌ 方案A:JVM层面实现泛型 → 旧字节码无法运行,破坏兼容性
  2. ✅ 方案B:编译器层面实现泛型 → 编译后擦除信息,运行时与旧代码一致

代价与限制:

// 限制1:不能用基本类型
List<int> list;  // ❌ 编译错误
// 原因:擦除后变成List,int无法转为Object

// 限制2:不能创建泛型数组
T[] array = new T[10];  // ❌ 编译错误
// 原因:擦除后不知道具体类型,无法分配内存

// 限制3:不能用instanceof
if (obj instanceof List<String>) { }  // ❌ 编译错误
// 原因:运行时List<String>和List<Integer>是同一个类型

3.3 擦除规则

泛型声明 擦除后类型 说明
List<String> List 原始类型(Raw Type)
<T> Object 无界类型参数
<T extends Number> Number 有界类型参数,擦除为上界
<T extends Comparable<T>> Comparable 擦除为第一个上界

示例验证:

// 源码
public class Box<T extends Number> {
    private T value;
    public void set(T value) { this.value = value; }
    public T get() { return value; }
}

// 擦除后等价于
public class Box {
    private Number value;  // T → Number
    public void set(Number value) { this.value = value; }
    public Number get() { return value; }
}

3.4 字节码分析:编译器的"魔法"

① 为什么get()不需要强转?

List<String> list = new ArrayList<>();
list.add("hello");
String str = list.get(0);  // 看起来没有强转

字节码分析(javap -c):

// Java代码
String str = list.get(0);

// 字节码
 0: aload_1               // 加载list
 1: iconst_0              // 加载参数0
 2: invokeinterface #2    // 调用get方法,返回Object
 7: checkcast #3          // ✅ 强转为String!
10: astore_2              // 存储到str变量

真相: 编译器自动插入了 checkcast 指令(强制类型转换),所以我们的Java代码中不需要手写。

② 为什么反射能获取泛型信息?

问题: 类型擦除后,为什么反射还能获取泛型?

public class Demo {
    private List<String> stringList;  // 成员变量

    public List<Integer> getIntList() {  // 方法返回值
        return null;
    }

    public void process(List<Double> doubleList) {  // 方法参数
    }
}

// 反射获取泛型信息
Field field = Demo.class.getDeclaredField("stringList");
Type genericType = field.getGenericType();
System.out.println(genericType);  // java.util.List<java.lang.String>

字节码分析(javap -v):

// .class文件中的签名信息(Signature)
Signature: Ljava/util/List<Ljava/lang/String;>;
           ↑
    泛型信息被保留在这里!

真相: .class文件中保留了签名(Signature)信息,反射通过读取Signature获取泛型类型。

关键限制:仅3种情况能获取泛型信息

// ✅ 可以获取
1. 成员变量的泛型:Field.getGenericType()
2. 方法参数的泛型:Method.getGenericParameterTypes()
3. 方法返回值的泛型:Method.getGenericReturnType()

// ❌ 无法获取
4. 局部变量的泛型
void method() {
    List<String> localList = new ArrayList<>();  // 局部变量无签名信息
}

3.5 桥接方法(Bridge Method)

经典面试题:子类重写泛型方法时发生了什么?

// 父类
public class ArrayList<E> {
    public boolean add(E e) { ... }
}

// 子类
public class StringList extends ArrayList<String> {
    @Override
    public boolean add(String s) {  // 我们重写了add(String)
        System.out.println("add String: " + s);
        return super.add(s);
    }
}

问题: 擦除后父类的add方法参数是Object,子类是String,这不算重写!

编译器的解决方案:生成桥接方法

// 编译后实际生成了两个方法

// 用户写的
public boolean add(String s) {
    System.out.println("add String: " + s);
    return super.add(s);
}

// 编译器自动生成的桥接方法
public boolean add(Object o) {  // 参数是Object,真正重写父类方法
    return add((String) o);     // 强转后调用用户写的方法
}

验证:

StringList list = new StringList();
Method[] methods = list.getClass().getDeclaredMethods();
for (Method m : methods) {
    System.out.println(m.getName() + " - " + m.isBridge());
}

// 输出:
// add - false  (用户写的)
// add - true   (编译器生成的桥接方法)

四、通配符(?)与上下界

4.1 为什么需要通配符?

// 需求:打印任意数字列表
public void printNumbers(List<Number> numbers) {
    for (Number n : numbers) {
        System.out.println(n);
    }
}

List<Integer> intList = Arrays.asList(1, 2, 3);
printNumbers(intList);  // ❌ 编译错误!
// 原因:List<Integer> 不是 List<Number> 的子类型

泛型的不可变性(Invariance):

// 即使 Integer 是 Number 的子类
Integer extends Number

// 但 List<Integer> 不是 List<Number> 的子类
List<Integer> ≠ List<Number>

解决方案:通配符

public void printNumbers(List<? extends Number> numbers) {
    // ? extends Number 表示"Number或其子类"
}

printNumbers(Arrays.asList(1, 2, 3));     // ✅ List<Integer>
printNumbers(Arrays.asList(1.1, 2.2));    // ✅ List<Double>
printNumbers(Arrays.asList(1L, 2L));      // ✅ List<Long>

4.2 三种通配符对比

写法 能否读? 能否写? 典型场景
List<T> ✅ 读出T ✅ 写入T 具体类型,功能最全
List<?> ✅ 读出Object ❌ 只能写null 未知类型,只读
List<? extends T> ✅ 读出T ❌ 只能写null 上界,只读(生产者)
List<? super T> ⚠️ 读出Object ✅ 写入T及其子类 下界,只写(消费者)

4.3 上界通配符 ? extends T(生产者)

List<? extends Number> list = new ArrayList<Integer>();

// ✅ 可以读取(返回Number)
Number n = list.get(0);

// ❌ 不能写入(除了null)
list.add(100);           // ❌ 编译错误
list.add(1.5);           // ❌ 编译错误
list.add(null);          // ✅ 可以,null是所有类型的子类

为什么不能写?

编译器的视角:
list可能是 List<Integer>
list也可能是 List<Double>
list还可能是 List<Long>

如果允许 list.add(1.5):
- 若实际是 List<Integer> → 类型污染!
- 若实际是 List<Double> → 正常

为了类型安全,干脆禁止所有写入操作

4.4 下界通配符 ? super T(消费者)

List<? super Integer> list = new ArrayList<Number>();

// ✅ 可以写入(Integer及其子类)
list.add(100);           // ✅ Integer
list.add(new Integer(200)); // ✅ Integer

// ⚠️ 读取只能用Object接收
Integer i = list.get(0);  // ❌ 编译错误
Object o = list.get(0);   // ✅ 可以

为什么能写?

编译器的视角:
list可能是 List<Integer>
list也可能是 List<Number>
list还可能是 List

但无论哪种情况,Integer的所有父类都能接受Integer
所以写入Integer是类型安全的

五、PECS原则(Producer Extends, Consumer Super)

核心口诀:频繁读取用 extends,频繁写入用 super

场景 推荐写法 原因
只从集合取数据(生产者) List<? extends T> Producer → Extends
只往集合存数据(消费者) List<? super T> Consumer → Super
既读又写 List<T>(具体类型) 通配符无法满足

经典案例:Collections.copy

public static <T> void copy(
    List<? super T> dest,      // 消费者:接收数据
    List<? extends T> src      // 生产者:提供数据
) {
    for (int i = 0; i < src.size(); i++) {
        dest.set(i, src.get(i));  // src生产,dest消费
    }
}

// 使用示例
List<Number> dest = new ArrayList<>();
List<Integer> src = Arrays.asList(1, 2, 3);
Collections.copy(dest, src);  // ✅ 类型安全

六、协变与逆变

6.1 核心概念

协变(Covariance): 子类型可以替换父类型

class Animal {}
class Dog extends Animal {}

// 上界通配符 = 协变
List<? extends Animal> animals;
animals = new ArrayList<Dog>();  // ✅ 子类型替换父类型

逆变(Contravariance): 父类型可以替换子类型

// 下界通配符 = 逆变
List<? super Dog> dogs;
dogs = new ArrayList<Animal>();  // ✅ 父类型替换子类型

6.2 应用场景

类型 方向 关键词 典型场景
协变 子→父 输出 方法返回值、数据提供者
逆变 父→子 输入 方法参数、数据消费者

七、泛型的边界限制

7.1 不能创建泛型数组

List<String>[] array = new List<String>[10];  // ❌ 编译错误

原因:数组协变 + 泛型擦除 = 类型不安全

// 假设允许创建泛型数组
List<String>[] array = new List<String>[10];

// 数组是协变的
Object[] objArray = array;

// 可以放入List<Integer>(运行时都是List)
objArray[0] = new ArrayList<Integer>();

// 取出时期望String,实际是Integer
String s = array[0].get(0);  // 💥 ClassCastException

解决方案:

// 方案1:使用集合(推荐)
List<List<String>> listOfLists = new ArrayList<>();

// 方案2:强制压制警告(不推荐)
@SuppressWarnings("unchecked")
List<String>[] array = (List<String>[]) new List[10];

7.2 不能实例化类型参数

public class Box<T> {
    private T value = new T();  // ❌ 编译错误
}

原因: 擦除后不知道T的具体类型,无法调用构造器

解决方案:

// 方案1:传入Class对象
public class Box<T> {
    private T value;
    public Box(Class<T> clazz) throws Exception {
        value = clazz.getDeclaredConstructor().newInstance();
    }
}

// 方案2:使用工厂方法
public interface Factory<T> {
    T create();
}

public class Box<T> {
    private T value;
    public Box(Factory<T> factory) {
        value = factory.create();
    }
}

八、高频面试题

Q1: 什么是泛型擦除?为什么要擦除?

答:

  • 定义: 编译后所有泛型信息被删除,字节码中只剩原始类型
  • 原因: 向后兼容Java 5之前的代码
  • 影响: 运行时无法获取泛型实际类型,不能创建泛型数组

Q2: List<String>List<Integer> 运行时是同一个类型吗?

答: 是的,都是ArrayList.class(擦除后)

Q3: 为什么get()不需要强转?

答: 编译器自动插入了checkcast指令(字节码层面的强转)

Q4: 既然擦除了,为什么反射能获取泛型?

答: .class文件中保留了Signature签名信息

  • ✅ 可获取:成员变量、方法参数、方法返回值的泛型
  • ❌ 无法获取:局部变量的泛型

Q5: 为什么泛型不支持基本类型?

答: 擦除后变成Object,基本类型无法转为Object(需要装箱)

Q6: List<? extends Number> 为什么不能add?

答: 编译器不知道具体是Integer还是Double,为防止类型污染,禁止写入

Q7: PECS原则怎么记?

答: Producer Extends(生产者用extends),Consumer Super(消费者用super)

Q8: 为什么不能创建泛型数组?

答: 数组协变+泛型擦除会破坏类型安全

Q9: 什么是桥接方法?

答: 编译器自动生成的方法,用于解决泛型擦除后重写方法参数类型不匹配的问题

Q10: 泛型在继承中的表现?

答: List<Integer> 不是 List<Number> 的子类(泛型不可变)


九、最佳实践

✅ 推荐做法

// 1. 优先使用具体类型
List<String> list = new ArrayList<>();

// 2. 需要灵活性时用通配符
void print(List<? extends Number> numbers) { }
void add(List<? super Integer> list) { }

// 3. 工具类方法用泛型方法
public static <T> T getFirst(List<T> list) { }

// 4. 避免原始类型
List<String> list = new ArrayList<>();  // 而非 List list

❌ 避免做法

// 1. 不要使用原始类型
List list = new ArrayList();  // 失去泛型意义

// 2. 不要忽略编译警告
@SuppressWarnings("unchecked")  // 除非真的知道自己在干嘛

// 3. 不要创建泛型数组
new List<String>[10]  // 非法

相关链接


记忆口诀

泛型擦除记心间,编译检查运行删 上界extends只能读,下界super才能写 PECS原则要记牢,生产extends消费super 桥接方法编译生,类型安全保无恙 数组泛型不能建,反射获取有限制


⬅️ Class对象与反射机制 🏠 00-Java ➡️ 注解原理