Java学习笔记之泛型

前言

写 Java 代码时,你一定见过 List<String>Map<Integer, String> 这种尖括号写法。这就是泛型 (Generics)------Java 5 引入的最重要的语言特性之一。在没有泛型的时代,集合里塞什么都可以,取出来必须强制转型,稍不注意就 ClassCastException(类转型异常)。泛型的出现让类型安全从运行时提到了编译期。

但这只是泛型的冰山一角。泛型真正的难点在于类型擦除通配符PECS 原则------理解了这些,你才算真正掌握了泛型。


一、概念:什么是泛型

1.1 从一个问题开始

在没有泛型的 Java 1.4 时代,集合是这样用的:

java 复制代码
// 没有泛型的时代
List list = new ArrayList();
list.add("hello");
list.add(123);        // 可以塞任何东西
list.add(new Date());

// 取出来必须强制转型
String s = (String) list.get(0);  // 正常
String s2 = (String) list.get(1); // ClassCastException(类转型异常)!运行时炸了

问题出在哪?

  1. 类型不安全------集合里什么都能放,编译器不会阻止你放错类型
  2. 需要强制转型------每次取元素都要手动转,代码臃肿
  3. 运行时才报错------类型错误拖到运行时才发现,排查困难

1.2 泛型的定义

泛型就是"参数化类型"------把类型当作参数,让同一套代码能处理不同类型的数据。

一句话理解:泛型就是把"类型"从写死的具体类,变成可以传的参数------就像方法可以传值参数一样,泛型让类/接口/方法可以传"类型参数"。

java 复制代码
// 有了泛型:在编译期就确定了集合里只能放 String
List<String> list = new ArrayList<>();
list.add("hello");
// list.add(123);  // 编译错误!直接拦截

// 取出来不需要强制转型
String s = list.get(0);  // 编译器知道一定是 String

泛型能做三件事:

泛型类型 语法 示例
泛型类 class 类名<T> class Box<T>
泛型接口 interface 接口名<T> interface List<T>
泛型方法 <T> 返回值 方法名(T t) <T> T getFirst(List<T> list)
java 复制代码
// 泛型类:一个能装任何类型的"盒子"
public class Box<T> {
    private T item;

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

    public T get() {
        return item;
    }
}
java 复制代码
// 使用:同一个 Box 类,装不同类型
Box<String> stringBox = new Box<>();
stringBox.set("hello");
String s = stringBox.get();     // 不用强转

Box<Integer> intBox = new Box<>();
intBox.set(123);
Integer i = intBox.get();       // 不用强转
java 复制代码
// 泛型接口:定义通用的比较器,可以比较任意类型
public interface Comparator<T> {
    int compare(T a, T b);
}
java 复制代码
// 实现泛型接口------两种方式

// 方式一:实现时确定类型(T 替换为具体类型)
public class StringComparator implements Comparator<String> {
    @Override
    public int compare(String a, String b) {
        return a.length() - b.length();  // 按字符串长度比较
    }
}

// 方式二:实现时保留泛型参数(T 继续传递给子类)
public class GenericComparator<T extends Comparable<T>> implements Comparator<T> {
    @Override
    public int compare(T a, T b) {
        return a.compareTo(b);  // 因为 T extends Comparable,可以调用 compareTo
    }
}
java 复制代码
// 使用
Comparator<String> sc = new StringComparator();
System.out.println(sc.compare("hello", "hi"));  // 3(5 - 2)

Comparator<Integer> gc = new GenericComparator<>();
System.out.println(gc.compare(10, 20));  // -1(10 < 20)
java 复制代码
// 泛型方法:方法自己定义类型参数,独立于类的泛型参数
public class Utils {
    // <T> 声明这是一个泛型方法,T 是类型参数
    // 返回值 T,参数类型也是 T
    public static <T> T getFirst(List<T> list) {
        return list.isEmpty() ? null : list.get(0);
    }

    // 泛型方法也可以有多个类型参数
    public static <K, V> Map<K, V> singletonMap(K key, V value) {
        Map<K, V> map = new HashMap<>();
        map.put(key, value);
        return map;
    }
}
java 复制代码
// 使用
String first = Utils.getFirst(List.of("a", "b", "c"));
System.out.println(first);  // a

Integer firstNum = Utils.getFirst(List.of(10, 20, 30));
System.out.println(firstNum);  // 10

Map<String, Integer> map = Utils.singletonMap("age", 25);
System.out.println(map);  // {age=25}

二、性质:泛型的完整特性

2.1 类型参数的命名惯例

字母 含义 常见场景
E Element 集合元素(如 List<E>
K Key 键(如 Map<K, V>
V Value 值(如 Map<K, V>
T Type 通用类型
S, U, V 第二、第三个类型 多参数场景
? 通配符 表示未知类型

2.2 泛型的边界(限定类型参数)

有时候你不希望泛型接受任意类型,而是限制它必须是某个类的子类或某个接口的实现:

java 复制代码
// 上界:T 必须是 Number 或 Number 的子类
// Number 是 Integer、Double、Long 等数字类型的父类
public class MathBox<T extends Number> {
    private T number;

    public MathBox(T number) {
        this.number = number;
    }

    // 因为 T 一定是 Number,所以可以调用 Number 的方法
    public double sqrt() {
        return Math.sqrt(number.doubleValue());
    }
}
java 复制代码
// 使用
MathBox<Integer> intBox = new MathBox<>(100);
System.out.println(intBox.sqrt());  // 10.0

MathBox<Double> doubleBox = new MathBox<>(2.25);
System.out.println(doubleBox.sqrt());  // 1.5

// MathBox<String> stringBox = new MathBox<>("hello");  // 编译错误!String 不是 Number 的子类

多边界(类 + 接口):

java 复制代码
// T 必须同时满足:是 Animal 的子类 + 实现了 Comparable 接口
// 类边界写在前面,接口边界写在后面(和 extends 规则一致)
public class SortedPair<T extends Animal & Comparable<T>> {
    // & 表示"且"------同时满足两个条件
}

2.3 类型擦除(最核心的特性)

类型擦除是泛型最反直觉的特性,也是理解泛型一切限制的钥匙。

类型擦除的含义 :Java 的泛型只在编译期 存在。编译完成后,所有的泛型类型参数都会被"擦除"------替换为它们的上界(如果没有指定上界就是 Object),并在必要的地方插入强制转型。

java 复制代码
// 你写的代码
List<String> stringList = new ArrayList<>();
stringList.add("hello");
String s = stringList.get(0);

// 编译后等价于
List stringList = new ArrayList();    // 类型参数被擦除
stringList.add("hello");
String s = (String) stringList.get(0); // 编译器自动插入强转

验证类型擦除:

java 复制代码
List<String> stringList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();

// 两个 List 的 Class 对象是同一个!
System.out.println(stringList.getClass() == intList.getClass());  // true

// 获取泛型信息:运行时刻根本不知道 T 是什么
System.out.println(stringList.getClass());  // class java.util.ArrayList
// 看不到 <String>,它已经被擦除了

类型擦除的后果:

java 复制代码
// 1. 不能实例化类型参数
public class Box<T> {
    // T item = new T();  // 编译错误!运行时 T 已经被擦除为 Object,不知道具体类型
}
java 复制代码
// 2. 不能创建泛型数组
// List<String>[] array = new List<String>[10];  // 编译错误!
java 复制代码
// 3. instanceof 不能用于泛型类型
// if (obj instanceof List<String>)  // 编译错误!运行时没有 <String> 这个信息
java 复制代码
// 4. 不能重载泛型方法(擦除后签名相同)
public class Overload {
    // 这两个方法擦除后参数都是 List,冲突!
    // public void print(List<String> list) { }
    // public void print(List<Integer> list) { }
}
java 复制代码
// 5. 静态字段不能使用类的泛型参数
public class Box<T> {
    // static T item;  // 编译错误!静态字段属于类,不属于实例,T 无法确定
}

为什么 Java 要类型擦除?

Java 设计泛型时,为了向后兼容------已有的海量 Java 代码不能因为加了泛型就编译不过。通过类型擦除,带泛型的新代码和不带泛型的老代码可以在同一个 JVM 上运行,泛型只是编译期的"语法糖"。

语法糖:让代码写起来更方便、更易读的语法,编译后会变成更基础的形式------泛型编译后变成强转,增强 for 循环编译后变成迭代器,都是语法糖。

2.4 泛型的继承关系

泛型的继承关系和普通类不同------这是初学者最容易踩的坑:

java 复制代码
// Integer 是 Number 的子类
Integer i = 10;
Number n = i;  // 可以

// 但 List<Integer> 不是 List<Number> 的子类!
List<Integer> intList = new ArrayList<>();
// List<Number> numList = intList;  // 编译错误!不兼容的类型

// 为什么?如果允许:
// List<Integer> intList = new ArrayList<>();
// List<Number> numList = intList;  // 假设允许
// numList.add(3.14);               // 往"看起来是 Number 列表"里加 Double
// Integer i = intList.get(0);      // ClassCastException(类转型异常)!取出来是 Double

结论:泛型类型之间没有继承关系,即使它们的类型参数之间有继承关系。 要表达"某种类型的子类型列表",需要用通配符


三、通配符:泛型的灵活性来源

3.1 无界通配符:?

? 表示"某种未知类型",只能读不能写(除了 null):

java 复制代码
// 打印任意类型的 List
public static void printList(List<?> list) {
    // 不能添加元素(除了 null)
    // list.add("hello");  // 编译错误!不知道 ? 是什么类型

    // 只能读取,且读取出来是 Object 类型
    for (Object obj : list) {
        System.out.println(obj);
    }
}
java 复制代码
List<String> strings = List.of("a", "b", "c");
List<Integer> ints = List.of(1, 2, 3);

printList(strings);  // 都能传
printList(ints);

3.2 上界通配符:? extends T

? extends T 表示"T 或 T 的某个子类型"。只能读,不能写(除了 null)。

java 复制代码
// 计算数字列表的总和------接受 List<Integer>、List<Double>、List<Number> 等
public static double sum(List<? extends Number> list) {
    double total = 0;
    for (Number n : list) {
        total += n.doubleValue();  // 可以读------知道至少是 Number
    }
    // list.add(100);  // 编译错误!不知道具体是什么数字类型
    return total;
}
java 复制代码
List<Integer> ints = List.of(1, 2, 3);
List<Double> doubles = List.of(1.5, 2.5, 3.5);

System.out.println(sum(ints));    // 6.0
System.out.println(sum(doubles)); // 7.5

3.3 下界通配符:? super T

? super T 表示"T 或 T 的某个父类型"。只能写,读取出来是 Object。

java 复制代码
// 把数字放入集合------接受 List<Integer>、List<Number>、List<Object>
public static void addNumbers(List<? super Integer> list) {
    list.add(1);   // 可以写------因为 Integer 一定是 ? 的子类型
    list.add(2);
    list.add(3);
    // Integer i = list.get(0);  // 编译错误!get 返回的是 Object
}
java 复制代码
List<Integer> intList = new ArrayList<>();
List<Number> numList = new ArrayList<>();
List<Object> objList = new ArrayList<>();

addNumbers(intList);  // 都能传
addNumbers(numList);
addNumbers(objList);

3.4 PECS 原则

PECS(Producer Extends, Consumer Super),即"生产者上界,消费者下界",是使用通配符的黄金法则。

  • Producer(生产者) :你只从集合中取数据,集合是数据的"生产者"
  • Consumer(消费者) :你只往集合中放数据,集合是数据的"消费者"
  • Extends(上界) :对应 ? extends T,适合生产者
  • Super(下界) :对应 ? super T,适合消费者
复制代码
如果你只从集合中"取"数据(生产者) → 用 <? extends T>
如果你只往集合中"放"数据(消费者) → 用 <? super T>
如果你既要取又要放  → 不能用通配符,用确定的类型 <T>

经典案例:JDK 中的 Collections.copy

java 复制代码
// JDK 源码:Collections.copy 的签名
// src 是"生产者"------只从中取元素 → ? extends T
// dest 是"消费者"------只往里放元素   → ? super T
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));
    }
}
java 复制代码
// 使用 PECS 的灵活性
List<Number> dest = new ArrayList<>(List.of(0, 0, 0));   // dest 是 Number
List<Integer> src = List.of(1, 2, 3);                     // src 是 Integer

Collections.copy(dest, src);  // 完美!Integer 可以复制到 Number 列表
System.out.println(dest);     // [1, 2, 3]

PECS 记忆技巧:

复制代码
Producer   Extends      ------ PE
Consumer   Super        ------ CS
                         → PECS

? extends → 只能读 → 生产者(给你数据)
? super   → 只能写 → 消费者(接收你的数据)

3.5 通配符总结对照表

通配符 含义 能读吗 能写吗 读取类型 写入类型
? 任意类型 不能(除 null) Object
? extends T T 或其子类 不能(除 null) T
? super T T 或其父类 Object T 及其子类
<T>(确定类型) 具体的 T T T

四、场景:泛型的典型应用

4.1 容器类(最常见)

JDK 集合框架是泛型最广泛的应用:

java 复制代码
List<String> strings = new ArrayList<>();
Map<Integer, String> map = new HashMap<>();
Set<Double> prices = new HashSet<>();

// 泛型让集合在编译期就确定了元素类型,告别了 ClassCastException(类转型异常)

4.2 泛型 DAO(数据访问层)

java 复制代码
// 泛型接口:定义通用的 CRUD 操作
public interface BaseDao<T> {
    T findById(Long id);
    List<T> findAll();
    void save(T entity);
    void delete(Long id);
}
java 复制代码
// 具体实体
public class User {
    private Long id;
    private String name;
    // getter/setter ...
}
java 复制代码
// 具体 DAO 实现
public class UserDao implements BaseDao<User> {

    @Override
    public User findById(Long id) {
        // 从数据库查询 User
        return new User();
    }

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

    @Override
    public void save(User entity) {
        System.out.println("保存用户:" + entity);
    }

    @Override
    public void delete(Long id) {
        System.out.println("删除用户:" + id);
    }
}

4.3 泛型工具方法

java 复制代码
// 数组转 List
public class ArrayUtils {

    // 安全地从数组中取第一个元素
    public static <T> T first(T[] array) {
        if (array == null || array.length == 0) {
            return null;
        }
        return array[0];
    }

    // 交换数组中两个位置的元素
    public static <T> void swap(T[] array, int i, int j) {
        T temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }
}

4.4 泛型回调与策略

回调(Callback):你把一段代码"交给别人",让它在合适的时机帮你执行。比如你告诉快递员"送到后打电话给我","打电话给我"就是回调------你不需要一直等着,快递员完成后会主动通知你。

java 复制代码
// 泛型回调接口:定义"任务完成后做什么"
// T 是任务的返回结果类型------成功时拿到 T,失败时拿到异常
public interface Callback<T> {
    void onSuccess(T result);      // 成功回调:任务完成,拿到结果
    void onError(Exception e);     // 失败回调:任务出错,拿到异常
}
java 复制代码
// 异步任务工具:执行一个任务,完成后自动回调通知你
public class AsyncTask {

    // task:要执行的任务(Callable 是 JDK 自带的函数式接口,call() 返回 T)
    // callback:任务完成后的回调(成功调 onSuccess,失败调 onError)
    public static <T> void execute(Callable<T> task, Callback<T> callback) {
        try {
            T result = task.call();       // 执行任务,拿到结果
            callback.onSuccess(result);   // 成功了,回调通知
        } catch (Exception e) {
            callback.onError(e);          // 出错了,回调通知
        }
    }
}
java 复制代码
// 使用
AsyncTask.execute(

    // 第一个参数:任务本身(Lambda 写法,相当于 Callable<String>)
    // call() 的返回值就是 "任务完成"
    () -> {
        Thread.sleep(1000);  // 模拟耗时操作
        return "任务完成";
    },

    // 第二个参数:回调(匿名内部类,实现 Callback<String>)
    // String 对应上面任务的返回类型
    new Callback<String>() {
        @Override
        public void onSuccess(String result) {
            System.out.println("成功:" + result);  // 任务成功时执行
        }

        @Override
        public void onError(Exception e) {
            System.out.println("失败:" + e.getMessage());  // 任务失败时执行
        }
    }
);
// 输出:成功:任务完成

五、举例:完整的代码示例

5.1 泛型结果封装

在 Web 开发中,API 接口的返回值通常有固定格式:状态码 + 消息 + 数据。用泛型可以把这套格式封装成一个通用类,数据类型由 T 决定。

java 复制代码
// 通用的 API 返回结果封装
// T 是返回数据的类型------可以是 User、String、List<Order> 等任何类型
public class Result<T> {

    private int code;       // 状态码:200=成功,404=未找到,500=服务器错误 等
    private String message; // 提示消息:如 "成功"、"用户不存在"
    private T data;         // 返回的数据,类型由 T 决定

    // 构造方法是 private------外部不能直接 new,只能通过 success() 或 error() 创建
    private Result(int code, String message, T data) {
        this.code = code;
        this.message = message;
        this.data = data;
    }

    // 静态工厂方法:创建成功结果
    // <T> 是方法级别的泛型声明,和类的 <T> 是同一个字母但互相独立
    public static <T> Result<T> success(T data) {
        return new Result<>(200, "成功", data);
    }

    // 静态工厂方法:创建失败结果
    // data 传 null------失败时没有数据
    public static <T> Result<T> error(int code, String message) {
        return new Result<>(code, message, null);
    }

    // map 转换:把 Result<T> 变成 Result<R>
    // 比如 Result<User> → Result<String>(提取用户名)
    // Function<T, R> 是 JDK 自带的函数式接口:接收 T,返回 R
    public <R> Result<R> map(Function<T, R> mapper) {
        if (data == null) {
            return Result.error(code, message);  // 没有数据,直接返回失败结果
        }
        return Result.success(mapper.apply(data));  // 有数据,用 mapper 转换后包装成新 Result
    }

    public int getCode() { return code; }
    public String getMessage() { return message; }
    public T getData() { return data; }

    @Override
    public String toString() {
        return "Result{code=" + code + ", message='" + message + "', data=" + data + "}";
    }
}
java 复制代码
// 用户实体
public class User {
    private String name;
    private int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() { return name; }
    public int getAge() { return age; }

    @Override
    public String toString() {
        return "User{name='" + name + "', age=" + age + "}";
    }
}

使用:

java 复制代码
// 成功场景:返回一个 User 对象
Result<User> userResult = Result.success(new User("张三", 25));
System.out.println(userResult);
// Result{code=200, message='成功', data=User{name='张三', age=25}}

// 失败场景:没有数据,data 为 null
Result<User> errorResult = Result.error(404, "用户不存在");
System.out.println(errorResult);
// Result{code=404, message='用户不存在', data=null}

// map 转换:Result<User> → Result<String>
// User::getName 相当于 user -> user.getName(),提取用户名
Result<String> nameResult = userResult.map(User::getName);
System.out.println(nameResult);
// Result{code=200, message='成功', data=张三}

5.2 泛型数据容器------树节点

树(Tree) 是一种层次结构------一个节点可以有多个子节点,每个子节点只能有一个父节点。常见例子:公司组织架构、文件目录、菜单结构。

java 复制代码
// 泛型树节点------T 是节点存储的数据类型(可以是 String、User、Department 等)
public class TreeNode<T> {

    private T data;                              // 节点存储的数据
    private TreeNode<T> parent;                  // 父节点(根节点的 parent 为 null)
    private List<TreeNode<T>> children = new ArrayList<>();  // 子节点列表

    public TreeNode(T data) {
        this.data = data;
    }

    // 添加子节点:创建子节点 → 建立父子关系 → 加入子节点列表
    // 返回子节点,方便链式调用,如 ceo.addChild("A").addChild("B")
    public TreeNode<T> addChild(T childData) {
        TreeNode<T> child = new TreeNode<>(childData);
        child.parent = this;      // 子节点的父节点 = 当前节点
        children.add(child);      // 把子节点加入当前节点的子节点列表
        return child;
    }

    // 先序遍历:先处理自己,再依次处理每个子节点(递归)
    // printer 是一个函数:接收 T,返回 String------由调用方决定怎么格式化输出
    public void traversePreOrder(Function<T, String> printer) {
        System.out.println(printer.apply(data));  // 先打印自己
        for (TreeNode<T> child : children) {
            child.traversePreOrder(printer);       // 再递归打印每个子节点
        }
    }

    public T getData() { return data; }
    public List<TreeNode<T>> getChildren() { return children; }
}

使用:

java 复制代码
// 用树结构表示公司组织架构
//
//        CEO-张总
//       /        \
//  CTO-李总    CFO-王总
//   /    \        \
// 研发   测试    财务

TreeNode<String> ceo = new TreeNode<>("CEO-张总");       // 根节点
TreeNode<String> cto = ceo.addChild("CTO-李总");         // CEO 的子节点
TreeNode<String> cfo = ceo.addChild("CFO-王总");         // CEO 的子节点
cto.addChild("研发经理-小赵");  // CTO 的子节点
cto.addChild("测试经理-小钱");  // CTO 的子节点
cfo.addChild("财务主管-小孙");  // CFO 的子节点

// 遍历打印:s -> s 表示直接输出字符串本身
ceo.traversePreOrder(s -> s);
// CEO-张总
// CTO-李总
// 研发经理-小赵
// 测试经理-小钱
// CFO-王总
// 财务主管-小孙

六、反例:泛型的常见误用

6.1 滥用通配符

java 复制代码
// 反例:所有地方都用通配符
public class Service {
    public List<?> getData() { return new ArrayList<>(); }
    public void process(List<?> list) { /* 什么都不能做 */ }
    // 调用方拿到 List<?>,既不能读(只能读 Object),也不能写
}

正确做法:如果能确定类型,就用确定的类型参数:

java 复制代码
// 正确:用具体的类型参数
public class Service<T> {
    public List<T> getData() { return new ArrayList<>(); }
    public void process(List<T> list) {
        T first = list.get(0);  // 拿到具体类型
        list.add(first);        // 可以写入
    }
}

6.2 到处用原始类型(绕过泛型)

java 复制代码
// 反例:混用泛型和原始类型
List<String> strings = new ArrayList<>();
List raw = strings;      // 原始类型,绕过了泛型检查
raw.add(123);            // 编译不报错,运行时也不报错
String s = strings.get(0); // ClassCastException(类转型异常)!取出来是 Integer

正确做法:永远不要混用泛型和原始类型。

6.3 泛型参数过多

java 复制代码
// 反例:五个类型参数,读代码的人完全不知道每个是什么意思
public class ComplexMap<K, V, C, S, R> {
    // K = Key, V = Value, C = Context, S = Strategy, R = Result
    // 参数太多,理解和维护都很困难
}

正确做法 :泛型参数控制在 2 个以内(如 Map<K, V>),超过 3 个就要考虑拆分或用具体类组合。

6.4 在不需要泛型的地方强行用泛型

java 复制代码
// 反例:整个类只有一个方法用到 T,T 的意义也不大
public class Printer<T> {
    public void print(T item) {
        System.out.println(item);
    }
}
// 使用时要先 new Printer<String>(),不如直接定义一个 print(Object) 方法

正确做法:如果类型参数只在个别方法上使用,把它改成泛型方法:

java 复制代码
public class Printer {
    public <T> void print(T item) {
        System.out.println(item);
    }
}

七、速查清单

问题 答案
泛型是什么? 参数化类型------把类型当作参数传递
泛型有哪三种形式? 泛型类、泛型接口、泛型方法
类型擦除是什么? 编译后泛型类型被擦除,替换为上界或 Object,并插入强制转型
List<Integer>List<Number> 的子类吗? 不是。泛型类型之间没有继承关系
? 是什么? 无界通配符,表示未知类型
? extends T 是什么? 上界通配符,T 或其子类,只能读
? super T 是什么? 下界通配符,T 或其父类,只能写
PECS 是什么意思? Producer Extends, Consumer Super,即生产者上界,消费者下界
Producer 用什么? ? extends T(只从中取数据)
Consumer 用什么? ? super T(只往里放数据)
泛型能用基本类型吗? 不能,只能用引用类型(用包装类替代)
泛型能有 static 字段吗? 不能,静态字段不能用类的类型参数
泛型能 new T() 吗? 不能,因为类型擦除后不知道 T 的具体类型
<T extends Number> 是什么意思? T 必须是 Number 或其子类
<T extends A & B> 是什么意思? T 必须同时是 A 的子类和 B 的实现

八、面试口述:什么是泛型

泛型是 Java 面试中的必考重灾区 ,因为它不仅涉及语法,还涉及底层编译机制和类型系统。泛型出现的最大动机就是为了在编译期消除强制类型转换的 ClassCastException(类转型异常),让类型安全在编译期就得到保证。

以下按面试逻辑链整理:是什么(语法)→ 为什么这么设计(擦除/兼容)→ 带来了什么问题(限制)→ 怎么解决/规避(PECS/桥方法/反射)

基础回答:一句话概括

泛型是 Java 5 引入的参数化类型机制,让类和接口能够接受类型参数,同一套代码可以处理多种不同的数据类型。

泛型的核心价值有两个。第一,类型安全------把运行时才能发现的 ClassCastException(类转型异常)提前到编译期。比如 List<String> 只能放 String,放 Integer 编译器直接报错,取元素时也不需要手动强转。第二,代码复用------一个 List<T> 就能通用于 List<String>List<Integer> 等所有类型,不需要每种类型写一个集合类。

考点一:类型擦除(必问)

面试官:说说你对 Java 泛型类型擦除的理解?为什么要擦除?

参考回答

Java 的泛型是伪泛型------在编译阶段,所有的泛型信息都会被擦除掉,运行时不存在泛型信息。这个过程就是类型擦除。

  • 擦除规则
    • 无界泛型(<T>)擦除后替换为 Object
    • 有界泛型(<T extends Xxx>)擦除后替换为边界类 Xxx
  • 为什么要擦除(历史包袱):Java 1.5 才引入泛型,为了向下兼容 1.5 之前的字节码(让非泛型代码和泛型代码能跑在同一个 JVM 上),设计者选择了类型擦除这种折中方案

进阶连环炮:既然擦除了,为什么通过反射还能获取泛型信息?

:类型擦除只是把方法体内部的泛型擦除成了 Object,但类签名、字段签名、方法参数和返回值的泛型信息,以 Signature 的形式保留在了 Class 字节码的常量池中。所以通过反射 ParameterizedType 可以获取这些声明处的泛型信息(但局部变量的泛型确实拿不到)。

考点二:通配符与 PECS 原则(高频)

面试官<T><?><? extends T><? super T> 有什么区别?什么时候用?

参考回答

  • <T>:用于定义泛型类、接口或方法时声明类型变量
  • <?>:无界通配符,表示未知类型,只能读(读作 Object),不能写
  • <? extends T>:上界通配符,表示 T 或 T 的子类,只读不写(Producer Extends)
  • <? super T>:下界通配符,表示 T 或 T 的父类,只写不读(Consumer Super),读的话只能读成 Object

PECS 原则(Producer Extends, Consumer Super,即"生产者上界,消费者下界") :如果要从集合中读取 数据(生产者),用 extends;如果要往集合中写入 数据(消费者),用 super

java 复制代码
// 读取:希望读出 Apple 或 Fruit
List<? extends Fruit> producer = new ArrayList<Apple>();
Fruit f = producer.get(0);  // OK
// producer.add(new Apple());  // 编译报错!编译器不知道实际是 ArrayList<Apple> 还是 ArrayList<Banana>

// 写入:希望往里塞 Apple
List<? super Apple> consumer = new ArrayList<Fruit>();
consumer.add(new Apple());  // OK,不管实际是 List<Fruit> 还是 List<Object>,都能装下 Apple
// Apple a = consumer.get(0);  // 编译报错!只能用 Object 接收

考点三:桥方法(区分度极高)

面试官:既然泛型被擦除了,多态怎么生效?什么是桥方法?

参考回答

当子类实现/重写父类的泛型方法时,由于类型擦除,编译器会自动生成一个桥方法来维持多态。

java 复制代码
public class MyPair<T> {
    public void setFirst(T value) { ... }
}

public class DatePair extends MyPair<Date> {
    @Override
    public void setFirst(Date value) { ... }  // 你写的方法
}

类型擦除后,MyPairsetFirst 变成了 setFirst(Object)。而 DatePair 只有 setFirst(Date)。按理说 JVM 找不到重写方法,多态失效。

但编译器会在 DatePair 中自动生成一个桥方法:

java 复制代码
// 编译器生成的桥方法(你看不到,但确实存在)
public void setFirst(Object value) {
    setFirst((Date) value);  // 强制类型转换,调用你写的那个方法
}

这就是为什么父类引用指向子类对象时,调用 setFirst(Object) 依然能正确执行子类逻辑的原因。

考点四:泛型的限制(常考)

面试官 :泛型有哪些限制?为什么不能 new T()?为什么不能创建泛型数组?

参考回答(都因为类型擦除):

  1. 不能实例化类型参数new T() 非法,擦除后变成了 new Object(),毫无意义。通常通过传入 Class<T> 并反射实例化
  2. 不能是基本类型List<int> 错误,必须是 List<Integer>,因为擦除后是 Object,而基本类型不是 Object 的子类
  3. 不能创建泛型数组new T[]new List<String>[] 非法,因为数组是协变的,且在运行时必须知道确切的组件类型,这与泛型擦除冲突,会破坏类型安全
  4. 不能用于 instanceof 检查if (obj instanceof List<String>) 非法,因为运行时只有 List。正确写法是 if (obj instanceof List)
  5. 静态成员不能用类的泛型参数:静态变量和方法属于类,在实例化之前就已存在,此时 T 还未确定

考点五:泛型方法 vs 泛型类

面试官:泛型方法和泛型类有什么区别?

参考回答

泛型方法是在声明方法时定义自己的泛型类型,独立于类是否为泛型类 。关键点:声明泛型方法时,必须在返回值前加 <T>

java 复制代码
// 这是一个普通类,但里面有泛型方法
public class Util {
    // 这里的 <T> 声明了这是一个泛型方法,与类无关
    public static <T> T getValue(T input) {
        return input;
    }
}

考点六:多重边界

面试官:泛型的边界可以指定多个吗?怎么写?

参考回答

可以,使用 & 符号。但有严格限制:类只能有一个,且必须放在最前面,接口可以有多个

java 复制代码
// 正确:T 必须是 Number 的子类,且实现了 Comparable 和 Serializable
<T extends Number & Comparable & Serializable>

// 错误:类没有放在最前面
// <T extends Comparable & Number & Serializable>
相关推荐
huangdong_1 小时前
有什么软件可以下载淘宝和天猫店铺的商品图片?——从工具推荐到技术原理的完整解答
java·前端·数据库
li星野1 小时前
RAG优化系列:自适应检索(Adaptive Retrieval)——让系统智能选择是否检索
人工智能·python·学习
两年半的个人练习生^_^1 小时前
Java String 全面解析:从源码到常量池,再到面试高频题
java·开发语言
weixin_BYSJ19871 小时前
springboot鹿邑县旅游网站99312(源码+文档)
java·javascript·spring boot·python·django·flask·php
七夜zippoe1 小时前
DolphinDB异常检测引擎:实时告警
java·服务器·网络·异常·告警·dolphindb
AOwhisky1 小时前
Ceph系列第四期:Ceph块存储(RBD)精讲
linux·运维·笔记·ceph·云计算·rbd
橙淮10 小时前
并发编程(六)
java·jvm
拽着尾巴的鱼儿10 小时前
springboot openfeign 自定义feign 接口重试机制
java·spring boot·后端
白露与泡影10 小时前
2026大厂Java面试题大全!牛客网最新版
java·开发语言