Java泛型详解

Java 泛型详解(2025–2026 面试/实战最实用版)

泛型(Generics)是 Java 5 引入的最重要特性之一,到今天仍然是面试、代码审查、框架源码阅读中最常考察的点。

下面按从基础到高阶的顺序,把最容易混淆、最常踩坑、最有价值的内容全部梳理一遍。

1. 为什么要有泛型?(最本质的两个原因)

  1. 类型安全(编译期发现错误)
List list = new ArrayList();
list.add("hello");
list.add(123);           // 编译通过
String s = (String) list.get(1);  // 运行时 ClassCastException
  1. 消除显式类型转换(代码更简洁)
// 没有泛型
String s = (String) list.get(0);

// 有泛型
List<String> list = new ArrayList<>();
String s = list.get(0);   // 无需强转

2. 泛型最核心的三种写法(必须记住)

写法位置语法示例含义出现频率
类/接口class Box<T>类型参数 T,定义在类/接口级别★★★★★
方法<E> void print(E e)方法级类型参数(独立于类泛型)★★★★☆
变量/参数List<String> list使用时指定具体类型★★★★★

3. 泛型擦除(Erasure)—— 面试必考点

核心结论
Java 泛型是编译期语法糖,在编译后会被擦除(类型参数被替换为 Object 或上界类型)。

擦除前后对比(字节码层面)

// 源代码
List<String> list = new ArrayList<>();
list.add("hello");

// 编译后(大致)
List list = new ArrayList();
list.add("hello");           // add(Object)
String s = (String) list.get(0);

擦除带来的重要影响(常考)

  1. 运行时无法获得泛型具体类型
List<String> list = new ArrayList<>();
System.out.println(list.getClass() == ArrayList.class);     // true
System.out.println(list instanceof List<String>);           // 编译错误!

// 运行时只能判断原始类型
System.out.println(list instanceof List);                   // true
  1. 静态变量/方法不能使用泛型类型参数
class Box<T> {
    static T value;           // 编译错误!
    static void set(T t) {}   // 编译错误!
}
  1. 不能创建泛型数组(new T[])
T[] arr = new T[10];          // 编译错误

4. 通配符(Wildcard)—— 最容易混淆的部分

通配符写法含义能读(get)能写(add)经典使用场景
List<?>任意类型只能得到 Object几乎不能写(只能加 null)作为只读参数
List<? extends Number>Number 或其子类可以 get 为 Number不能写“生产者”(Producer Extends)
List<? super Integer>Integer 的父类(包括 Integer)只能得到 Object可以写 Integer 及其子类“消费者”(Consumer Super)

经典面试题:PECS 原则

Producer Extends:如果你要从集合中读取数据,用 ? extends
Consumer Super :如果你要向集合中写入数据,用 ? super

记忆口诀
“读取用 extends,写入用 super”

5. 泛型方法(最常被忽视但非常强大)

// 最常见的三种泛型方法写法

// 1. 普通泛型方法(最常用)
public static <T> void print(T t) {
    System.out.println(t);
}

// 2. 带返回值的泛型方法
public static <T> T getFirst(List<T> list) {
    return list.isEmpty() ? null : list.get(0);
}

// 3. 泛型方法 + 多个类型参数
public static <K, V> Map<K, V> newMap() {
    return new HashMap<>();
}

注意:泛型方法前的 <T>方法级别的类型参数,与类泛型无关。

6. 泛型在集合框架中的真实用法(面试常考)

// 生产者(只读)
void printNumbers(List<? extends Number> list) {
    for (Number n : list) {     // 安全读取
        System.out.println(n);
    }
    // list.add(1);             // 编译错误
}

// 消费者(只写)
void addIntegers(List<? super Integer> list) {
    list.add(1);                // 安全写入
    list.add(100);              // OK
    // Integer x = list.get(0); // 编译错误,只能得到 Object
}

7. 2025–2026 年高频面试/实战问题

  1. 为什么 List<String> 不是 List<Object> 的子类型?(类型不变量)
  2. <?><? extends T><? super T> 分别能干什么、不能干什么?
  3. 泛型擦除后如何实现类型安全?(编译器插入强制类型转换)
  4. 为什么静态方法/变量不能使用类泛型参数?
  5. 泛型数组为什么不允许 new T[]?(可以用反射绕过,但不推荐)
  6. 如何创建一个带有泛型类型的数组?(最安全写法:(T[]) new Object[size]

8. 一句话总结 + 记忆口诀

一句话本质
Java 泛型是编译期类型检查 + 语法糖,运行时全部擦除为原始类型(或上界类型),靠编译器帮我们做类型转换。

最实用口诀(背下来就过关):

  • 类写 <T>,用时指定类型
  • 方法写 <T>,独立于类泛型
  • 读取用 extends,写入用 super
  • 运行时全擦除,类型安全靠编译器
  • 静态成员别用 T,数组创建别 new T

如果你能手写下面三段代码,就基本掌握泛型 80%:

  1. 带上下界的泛型方法
  2. PECS 原则的两个典型方法
  3. 解释 List<String> 为什么不能赋值给 List<Object>

需要更深入哪一块?
比如:

  • 泛型在反射中的真实写法
  • 签名擦除与桥接方法(bridge method)
  • 泛型在 Lambda / Stream 中的常见坑
  • 面试真题:写一个泛型版的 max 函数

随时告诉我,我继续给你展开~

文章已创建 3958

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

相关文章

开始在上面输入您的搜索词,然后按回车进行搜索。按ESC取消。

返回顶部