八股-泛型

泛型

1. 基础概念

定义:泛型是 JDK 5 引入的特性,允许在定义类、接口、方法时使用类型参数(Type Parameters),使用时再用具体类型替换。

本质:参数化类型(Parameterized Types),将数据类型作为参数传递。

2. 使用泛型的核心优势

2.1 编译期类型安全(Type Safety)

问题背景:JDK 5 之前,集合使用 Object 存储元素,取出时需强制类型转换,运行时易发生 ClassCastException

1
2
3
4
5
// 无泛型时代码(错误示例)
List list = new ArrayList();
list.add("string");
list.add(100); // 编译通过,运行时才暴露问题
Integer num = (Integer) list.get(0); // ClassCastException

泛型解决方案

1
2
3
4
5
// 使用泛型
List<String> list = new ArrayList<>();
list.add("string");
// list.add(100); // 编译期直接报错,将类型错误提前发现
String str = list.get(0); // 无需强制转换,类型安全

关键点:类型检查从运行时提前到编译时,符合”Fail Fast”原则。

2.2 代码复用与算法抽象

编写与类型无关的通用逻辑,避免为不同数据类型重复写相似代码。

1
2
3
4
5
6
7
// 泛型方法示例:交换任意类型的数组元素
public static <T> void swap(T[] arr, int i, int j) {
T temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
// 可用于 Integer[]、String[]、自定义对象数组等

3. 类型擦除(Type Erasure)

3.1 什么是类型擦除?

Java 泛型是伪泛型,编译器在编译阶段将泛型信息擦除,替换为边界类型(默认 Object,有界泛型则替换为边界),生成的字节码中不包含泛型类型信息。

编译前后对比

1
2
3
4
5
6
7
// 源代码
List<String> list = new ArrayList<>();
String s = list.get(0);

// 编译后(反编译结果,伪代码)
List list = new ArrayList();
String s = (String) list.get(0); // 强制转换由编译器自动插入

3.2 为什么要类型擦除?—— 向后兼容性(Backward Compatibility)

历史背景:Java 5 引入泛型时,已有大量非泛型代码运行于旧版 JVM,且不允许修改 JVM 指令集。

设计权衡

  • 目标:新泛型代码能跑在旧 JVM 上,且新旧代码能互操作
  • 方案:类型擦除,仅在编译期做类型检查,运行期无泛型信息
  • 代价:运行期无法获取泛型类型(如 T.class 不合法)

4. 类型擦除的具体规则与影响

4.1 擦除规则

泛型声明 擦除后类型
List<T> List(原始类型,元素视为 Object)
List<String> List
List<T extends Number> List<Number>
<T extends Comparable<T>> Comparable

4.2 带来的限制

  1. 不能创建类型参数实例
    new T() 非法,因为编译后变为 new Object()

  2. 不能创建泛型数组
    new T[10] 非法,可能导致数组存储异常

  3. 无法使用 instanceof 检查泛型
    if (obj instanceof List<String>) 编译错误,只能写 List<?>

  4. 静态变量共享

    1
    2
    3
    class Test<T> {
    static T value; // 编译错误,静态变量无法使用类型参数
    }
  5. 桥接方法(Bridge Method)
    泛型类继承时,编译器会自动生成桥接方法保持多态性(了解即可)。


5. 通配符与边界(进阶)

5.1 无界通配符 <?>

表示未知类型,用于只读场景:

1
2
3
4
5
6
public void printList(List<?> list) {
for (Object o : list) {
System.out.println(o);
}
}
// 可接受 List<String>、List<Integer> 等

5.2 上界通配符 <? extends T>

只读不写(除 null 外),适用于读取数据:

1
2
3
4
5
6
// 接受 Number 及其子类(Integer, Double等)
double sum(List<? extends Number> list) {
double total = 0;
for (Number n : list) total += n.doubleValue();
return total;
}

5.3 下界通配符 <? super T>

只写不读(读出来是 Object),适用于写入数据:

1
2
3
4
5
// 接受 Integer 及其父类(Number, Object)
void addNumbers(List<? super Integer> list) {
list.add(100); // 可以安全写入 Integer
// Integer i = list.get(0); // 编译错误,只能按 Object 读
}

PECS 原则:Producer-Extends, Consumer-Super(生产者用上界,消费者用下界)


6. 考点速记

问题 要点
泛型的好处? 编译期类型安全 + 消除强制转换 + 代码复用
类型擦除是什么? 编译后泛型变 Object/边界,字节码无泛型信息
为什么擦除? 兼容 JDK 5 之前的代码和 JVM
List 和 List 是同一类型吗? 运行时相同(都是 List),编译期不同
如何获取泛型类型? 运行时无法直接获取,但可通过反射获取父类/接口的泛型参数(如 getGenericSuperclass()
泛型数组为什么不行? 数组协变 + 类型擦除 = 可能破坏类型安全