Java学习笔记之泛型

作者:飞翔网日期:2026/6/4

前言

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

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


一、概念:什么是泛型

1.1 从一个问题开始

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

1// 没有泛型的时代
2List list = new ArrayList();
3list.add("hello");
4list.add(123);        // 可以塞任何东西
5list.add(new Date());
6
7// 取出来必须强制转型
8String s = (String) list.get(0);  // 正常
9String s2 = (String) list.get(1); // ClassCastException(类转型异常)!运行时炸了
10

问题出在哪?

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

1.2 泛型的定义

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

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

1// 有了泛型:在编译期就确定了集合里只能放 String
2List<String> list = new ArrayList<>();
3list.add("hello");
4// list.add(123);  // 编译错误!直接拦截
5
6// 取出来不需要强制转型
7String s = list.get(0);  // 编译器知道一定是 String
8

泛型能做三件事:

泛型类型语法示例
泛型类class 类名<T>class Box<T>
泛型接口interface 接口名<T>interface List<T>
泛型方法<T> 返回值 方法名(T t)<T> T getFirst(List<T> list)
1// 泛型类:一个能装任何类型的"盒子"
2public class Box<T> {
3    private T item;
4
5    public void set(T item) {
6        this.item = item;
7    }
8
9    public T get() {
10        return item;
11    }
12}
13
1// 使用:同一个 Box 类,装不同类型
2Box<String> stringBox = new Box<>();
3stringBox.set("hello");
4String s = stringBox.get();     // 不用强转
5
6Box<Integer> intBox = new Box<>();
7intBox.set(123);
8Integer i = intBox.get();       // 不用强转
9
1// 泛型接口:定义通用的比较器,可以比较任意类型
2public interface Comparator<T> {
3    int compare(T a, T b);
4}
5
1// 实现泛型接口——两种方式
2
3// 方式一:实现时确定类型(T 替换为具体类型)
4public class StringComparator implements Comparator<String> {
5    @Override
6    public int compare(String a, String b) {
7        return a.length() - b.length();  // 按字符串长度比较
8    }
9}
10
11// 方式二:实现时保留泛型参数(T 继续传递给子类)
12public class GenericComparator<T extends Comparable<T>> implements Comparator<T> {
13    @Override
14    public int compare(T a, T b) {
15        return a.compareTo(b);  // 因为 T extends Comparable,可以调用 compareTo
16    }
17}
18
1// 使用
2Comparator<String> sc = new StringComparator();
3System.out.println(sc.compare("hello", "hi"));  // 3(5 - 2)
4
5Comparator<Integer> gc = new GenericComparator<>();
6System.out.println(gc.compare(10, 20));  // -1(10 < 20)
7
1// 泛型方法:方法自己定义类型参数,独立于类的泛型参数
2public class Utils {
3    // <T> 声明这是一个泛型方法,T 是类型参数
4    // 返回值 T,参数类型也是 T
5    public static <T> T getFirst(List<T> list) {
6        return list.isEmpty() ? null : list.get(0);
7    }
8
9    // 泛型方法也可以有多个类型参数
10    public static <K, V> Map<K, V> singletonMap(K key, V value) {
11        Map<K, V> map = new HashMap<>();
12        map.put(key, value);
13        return map;
14    }
15}
16
1// 使用
2String first = Utils.getFirst(List.of("a", "b", "c"));
3System.out.println(first);  // a
4
5Integer firstNum = Utils.getFirst(List.of(10, 20, 30));
6System.out.println(firstNum);  // 10
7
8Map<String, Integer> map = Utils.singletonMap("age", 25);
9System.out.println(map);  // {age=25}
10

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

2.1 类型参数的命名惯例

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

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

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

1// 上界:T 必须是 Number  Number 的子类
2// Number  Integer、Double、Long 等数字类型的父类
3public class MathBox<T extends Number> {
4    private T number;
5
6    public MathBox(T number) {
7        this.number = number;
8    }
9
10    // 因为 T 一定是 Number,所以可以调用 Number 的方法
11    public double sqrt() {
12        return Math.sqrt(number.doubleValue());
13    }
14}
15
1// 使用
2MathBox<Integer> intBox = new MathBox<>(100);
3System.out.println(intBox.sqrt());  // 10.0
4
5MathBox<Double> doubleBox = new MathBox<>(2.25);
6System.out.println(doubleBox.sqrt());  // 1.5
7
8// MathBox<String> stringBox = new MathBox<>("hello");  // 编译错误!String 不是 Number 的子类
9

多边界(类 + 接口):

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

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

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

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

1// 你写的代码
2List<String> stringList = new ArrayList<>();
3stringList.add("hello");
4String s = stringList.get(0);
5
6// 编译后等价于
7List stringList = new ArrayList();    // 类型参数被擦除
8stringList.add("hello");
9String s = (String) stringList.get(0); // 编译器自动插入强转
10

验证类型擦除:

1List<String> stringList = new ArrayList<>();
2List<Integer> intList = new ArrayList<>();
3
4// 两个 List  Class 对象是同一个!
5System.out.println(stringList.getClass() == intList.getClass());  // true
6
7// 获取泛型信息:运行时刻根本不知道 T 是什么
8System.out.println(stringList.getClass());  // class java.util.ArrayList
9// 看不到 <String>,它已经被擦除了
10

类型擦除的后果:

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

为什么 Java 要类型擦除?

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

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

2.4 泛型的继承关系

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

1// Integer  Number 的子类
2Integer i = 10;
3Number n = i;  // 可以
4
5//  List<Integer> 不是 List<Number> 的子类!
6List<Integer> intList = new ArrayList<>();
7// List<Number> numList = intList;  // 编译错误!不兼容的类型
8
9// 为什么?如果允许:
10// List<Integer> intList = new ArrayList<>();
11// List<Number> numList = intList;  // 假设允许
12// numList.add(3.14);               // 往"看起来是 Number 列表"里加 Double
13// Integer i = intList.get(0);      // ClassCastException(类转型异常)!取出来是 Double
14

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


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

3.1 无界通配符:?

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

1// 打印任意类型的 List
2public static void printList(List<?> list) {
3    // 不能添加元素(除了 null)
4    // list.add("hello");  // 编译错误!不知道 ? 是什么类型
5
6    // 只能读取,且读取出来是 Object 类型
7    for (Object obj : list) {
8        System.out.println(obj);
9    }
10}
11
1List<String> strings = List.of("a", "b", "c");
2List<Integer> ints = List.of(1, 2, 3);
3
4printList(strings);  // 都能传
5printList(ints);
6

3.2 上界通配符:? extends T

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

1// 计算数字列表的总和——接受 List<Integer>、List<Double>、List<Number> 
2public static double sum(List<? extends Number> list) {
3    double total = 0;
4    for (Number n : list) {
5        total += n.doubleValue();  // 可以读——知道至少是 Number
6    }
7    // list.add(100);  // 编译错误!不知道具体是什么数字类型
8    return total;
9}
10
1List<Integer> ints = List.of(1, 2, 3);
2List<Double> doubles = List.of(1.5, 2.5, 3.5);
3
4System.out.println(sum(ints));    // 6.0
5System.out.println(sum(doubles)); // 7.5
6

3.3 下界通配符:? super T

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

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

3.4 PECS 原则

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

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

经典案例:JDK 中的 Collections.copy

1// JDK 源码:Collections.copy 的签名
2// src 是"生产者"——只从中取元素  ? extends T
3// dest 是"消费者"——只往里放元素    ? super T
4public static <T> void copy(List<? super T> dest, List<? extends T> src) {
5    for (int i = 0; i < src.size(); i++) {
6        dest.set(i, src.get(i));
7    }
8}
9
1// 使用 PECS 的灵活性
2List<Number> dest = new ArrayList<>(List.of(0, 0, 0));   // dest  Number
3List<Integer> src = List.of(1, 2, 3);                     // src  Integer
4
5Collections.copy(dest, src);  // 完美!Integer 可以复制到 Number 列表
6System.out.println(dest);     // [1, 2, 3]
7

PECS 记忆技巧:

1Producer   Extends      —— PE
2Consumer   Super        —— CS
3                          PECS
4
5 extends  只能读  生产者(给你数据)
6 super    只能写  消费者(接收你的数据)
7

3.5 通配符总结对照表

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

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

4.1 容器类(最常见)

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

1List<String> strings = new ArrayList<>();
2Map<Integer, String> map = new HashMap<>();
3Set<Double> prices = new HashSet<>();
4
5// 泛型让集合在编译期就确定了元素类型,告别了 ClassCastException(类转型异常)
6

4.2 泛型 DAO(数据访问层)

1// 泛型接口:定义通用的 CRUD 操作
2public interface BaseDao<T> {
3    T findById(Long id);
4    List<T> findAll();
5    void save(T entity);
6    void delete(Long id);
7}
8
1// 具体实体
2public class User {
3    private Long id;
4    private String name;
5    // getter/setter ...
6}
7
1// 具体 DAO 实现
2public class UserDao implements BaseDao<User> {
3
4    @Override
5    public User findById(Long id) {
6        // 从数据库查询 User
7        return new User();
8    }
9
10    @Override
11    public List<User> findAll() {
12        return new ArrayList<>();
13    }
14
15    @Override
16    public void save(User entity) {
17        System.out.println("保存用户:" + entity);
18    }
19
20    @Override
21    public void delete(Long id) {
22        System.out.println("删除用户:" + id);
23    }
24}
25

4.3 泛型工具方法

1// 数组转 List
2public class ArrayUtils {
3
4    // 安全地从数组中取第一个元素
5    public static <T> T first(T[] array) {
6        if (array == null || array.length == 0) {
7            return null;
8        }
9        return array[0];
10    }
11
12    // 交换数组中两个位置的元素
13    public static <T> void swap(T[] array, int i, int j) {
14        T temp = array[i];
15        array[i] = array[j];
16        array[j] = temp;
17    }
18}
19

4.4 泛型回调与策略

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

1// 泛型回调接口:定义"任务完成后做什么"
2// T 是任务的返回结果类型——成功时拿到 T,失败时拿到异常
3public interface Callback<T> {
4    void onSuccess(T result);      // 成功回调:任务完成,拿到结果
5    void onError(Exception e);     // 失败回调:任务出错,拿到异常
6}
7
1// 异步任务工具:执行一个任务,完成后自动回调通知你
2public class AsyncTask {
3
4    // task:要执行的任务(Callable  JDK 自带的函数式接口,call() 返回 T)
5    // callback:任务完成后的回调(成功调 onSuccess,失败调 onError)
6    public static <T> void execute(Callable<T> task, Callback<T> callback) {
7        try {
8            T result = task.call();       // 执行任务,拿到结果
9            callback.onSuccess(result);   // 成功了,回调通知
10        } catch (Exception e) {
11            callback.onError(e);          // 出错了,回调通知
12        }
13    }
14}
15
1// 使用
2AsyncTask.execute(
3
4    // 第一个参数:任务本身(Lambda 写法,相当于 Callable<String>)
5    // call() 的返回值就是 "任务完成"
6    () -> {
7        Thread.sleep(1000);  // 模拟耗时操作
8        return "任务完成";
9    },
10
11    // 第二个参数:回调(匿名内部类,实现 Callback<String>)
12    // String 对应上面任务的返回类型
13    new Callback<String>() {
14        @Override
15        public void onSuccess(String result) {
16            System.out.println("成功:" + result);  // 任务成功时执行
17        }
18
19        @Override
20        public void onError(Exception e) {
21            System.out.println("失败:" + e.getMessage());  // 任务失败时执行
22        }
23    }
24);
25// 输出:成功:任务完成
26

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

5.1 泛型结果封装

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

1// 通用的 API 返回结果封装
2// T 是返回数据的类型——可以是 User、String、List<Order> 等任何类型
3public class Result<T> {
4
5    private int code;       // 状态码:200=成功,404=未找到,500=服务器错误 
6    private String message; // 提示消息:如 "成功"、"用户不存在"
7    private T data;         // 返回的数据,类型由 T 决定
8
9    // 构造方法是 private——外部不能直接 new,只能通过 success()  error() 创建
10    private Result(int code, String message, T data) {
11        this.code = code;
12        this.message = message;
13        this.data = data;
14    }
15
16    // 静态工厂方法:创建成功结果
17    // <T> 是方法级别的泛型声明,和类的 <T> 是同一个字母但互相独立
18    public static <T> Result<T> success(T data) {
19        return new Result<>(200, "成功", data);
20    }
21
22    // 静态工厂方法:创建失败结果
23    // data  null——失败时没有数据
24    public static <T> Result<T> error(int code, String message) {
25        return new Result<>(code, message, null);
26    }
27
28    // map 转换:把 Result<T> 变成 Result<R>
29    // 比如 Result<User>  Result<String>(提取用户名)
30    // Function<T, R>  JDK 自带的函数式接口:接收 T,返回 R
31    public <R> Result<R> map(Function<T, R> mapper) {
32        if (data == null) {
33            return Result.error(code, message);  // 没有数据,直接返回失败结果
34        }
35        return Result.success(mapper.apply(data));  // 有数据,用 mapper 转换后包装成新 Result
36    }
37
38    public int getCode() { return code; }
39    public String getMessage() { return message; }
40    public T getData() { return data; }
41
42    @Override
43    public String toString() {
44        return "Result{code=" + code + ", message='" + message + "', data=" + data + "}";
45    }
46}
47
1// 用户实体
2public class User {
3    private String name;
4    private int age;
5
6    public User(String name, int age) {
7        this.name = name;
8        this.age = age;
9    }
10
11    public String getName() { return name; }
12    public int getAge() { return age; }
13
14    @Override
15    public String toString() {
16        return "User{name='" + name + "', age=" + age + "}";
17    }
18}
19

使用:

1// 成功场景:返回一个 User 对象
2Result<User> userResult = Result.success(new User("张三", 25));
3System.out.println(userResult);
4// Result{code=200, message='成功', data=User{name='张三', age=25}}
5
6// 失败场景:没有数据,data  null
7Result<User> errorResult = Result.error(404, "用户不存在");
8System.out.println(errorResult);
9// Result{code=404, message='用户不存在', data=null}
10
11// map 转换:Result<User>  Result<String>
12// User::getName 相当于 user -> user.getName(),提取用户名
13Result<String> nameResult = userResult.map(User::getName);
14System.out.println(nameResult);
15// Result{code=200, message='成功', data=张三}
16

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

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

1// 泛型树节点——T 是节点存储的数据类型(可以是 String、User、Department 等)
2public class TreeNode<T> {
3
4    private T data;                              // 节点存储的数据
5    private TreeNode<T> parent;                  // 父节点(根节点的 parent  null)
6    private List<TreeNode<T>> children = new ArrayList<>();  // 子节点列表
7
8    public TreeNode(T data) {
9        this.data = data;
10    }
11
12    // 添加子节点:创建子节点  建立父子关系  加入子节点列表
13    // 返回子节点,方便链式调用,如 ceo.addChild("A").addChild("B")
14    public TreeNode<T> addChild(T childData) {
15        TreeNode<T> child = new TreeNode<>(childData);
16        child.parent = this;      // 子节点的父节点 = 当前节点
17        children.add(child);      // 把子节点加入当前节点的子节点列表
18        return child;
19    }
20
21    // 先序遍历:先处理自己,再依次处理每个子节点(递归)
22    // printer 是一个函数:接收 T,返回 String——由调用方决定怎么格式化输出
23    public void traversePreOrder(Function<T, String> printer) {
24        System.out.println(printer.apply(data));  // 先打印自己
25        for (TreeNode<T> child : children) {
26            child.traversePreOrder(printer);       // 再递归打印每个子节点
27        }
28    }
29
30    public T getData() { return data; }
31    public List<TreeNode<T>> getChildren() { return children; }
32}
33

使用:

1// 用树结构表示公司组织架构
2//
3//        CEO-张总
4//       /        \
5//  CTO-李总    CFO-王总
6//   /    \        \
7// 研发   测试    财务
8
9TreeNode<String> ceo = new TreeNode<>("CEO-张总");       // 根节点
10TreeNode<String> cto = ceo.addChild("CTO-李总");         // CEO 的子节点
11TreeNode<String> cfo = ceo.addChild("CFO-王总");         // CEO 的子节点
12cto.addChild("研发经理-小赵");  // CTO 的子节点
13cto.addChild("测试经理-小钱");  // CTO 的子节点
14cfo.addChild("财务主管-小孙");  // CFO 的子节点
15
16// 遍历打印:s -> s 表示直接输出字符串本身
17ceo.traversePreOrder(s -> s);
18// CEO-张总
19// CTO-李总
20// 研发经理-小赵
21// 测试经理-小钱
22// CFO-王总
23// 财务主管-小孙
24

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

6.1 滥用通配符

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

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

1// 正确:用具体的类型参数
2public class Service<T> {
3    public List<T> getData() { return new ArrayList<>(); }
4    public void process(List<T> list) {
5        T first = list.get(0);  // 拿到具体类型
6        list.add(first);        // 可以写入
7    }
8}
9

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

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

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

6.3 泛型参数过多

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

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

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

1// 反例:整个类只有一个方法用到 T,T 的意义也不大
2public class Printer<T> {
3    public void print(T item) {
4        System.out.println(item);
5    }
6}
7// 使用时要先 new Printer<String>(),不如直接定义一个 print(Object) 方法
8

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

1public class Printer {
2    public <T> void print(T item) {
3        System.out.println(item);
4    }
5}
6

七、速查清单

问题答案
泛型是什么?参数化类型——把类型当作参数传递
泛型有哪三种形式?泛型类、泛型接口、泛型方法
类型擦除是什么?编译后泛型类型被擦除,替换为上界或 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

1// 读取:希望读出 Apple  Fruit
2List<? extends Fruit> producer = new ArrayList<Apple>();
3Fruit f = producer.get(0);  // OK
4// producer.add(new Apple());  // 编译报错!编译器不知道实际是 ArrayList<Apple> 还是 ArrayList<Banana>
5
6// 写入:希望往里塞 Apple
7List<? super Apple> consumer = new ArrayList<Fruit>();
8consumer.add(new Apple());  // OK,不管实际是 List<Fruit> 还是 List<Object>,都能装下 Apple
9// Apple a = consumer.get(0);  // 编译报错!只能用 Object 接收
10

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

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

参考回答

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

1public class MyPair<T> {
2    public void setFirst(T value) { ... }
3}
4
5public class DatePair extends MyPair<Date> {
6    @Override
7    public void setFirst(Date value) { ... }  // 你写的方法
8}
9

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

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

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

这就是为什么父类引用指向子类对象时,调用 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>

1// 这是一个普通类,但里面有泛型方法
2public class Util {
3    // 这里的 <T> 声明了这是一个泛型方法,与类无关
4    public static <T> T getValue(T input) {
5        return input;
6    }
7}
8

考点六:多重边界

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

参考回答

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

1// 正确:T 必须是 Number 的子类,且实现了 Comparable  Serializable
2<T extends Number & Comparable & Serializable>
3
4// 错误:类没有放在最前面
5// <T extends Comparable & Number & Serializable>
6

Java学习笔记之泛型》 是转载文章,点击查看原文


相关推荐


【SpringBoot+Elasticsearch 内容搜索系统实战】:架构设计与全流程实现
fengxin_rou2026/5/29

🔥你好我是fengxin_rou这是我的个人主页fengxin_rou的主页 ❄️欢迎查看我的专栏我的专栏 《Java后端学习》、《JAVASE基础》、《JUC并发》、《redis》、《JVM虚拟机》、《MYSQL》、《黑马点评》、《rabbitmq》、《JavaWeb+AI的talis学习系统》、《苍穹外卖》 目录 前言 一、Elasticsearch 索引设计与初始化 1.1 核心概念类比 1.2 索引初始化实现 1.3 字段设计要点 二、搜索索引数据写入与同步机


深入理解 Kotlin 协程 (六):进退有度,解密协程取消响应与异常分发机制
雨白2026/5/7

协程的取消机制 取消协程需要协程内部配合,这点和线程一样,本质上也是协作式的取消,就是将状态设置为取消,协程内部根据状态的变化来响应。 完善 Job 的状态流转与取消通知 我们基于上一篇博客中的代码,来完善协程的取消逻辑。 首先支持协程取消回调的注册: // [AbstractCoroutine.kt] override fun invokeOnCancel(onCancel: OnCancel): Disposable { // 1. 创建回调包装对象,以便后续可以手动解绑 v


Flink+Kafka:数据流处理实战指南
渣渣盟2026/4/27

目录 代码结构 代码解析 (1) 主程序入口 (2) 定义数据流 (3) 使用旧版 Kafka Sink (4) 使用新版 Kafka Sink (5) 将数据写入 Kafka (6) 执行任务 代码优化 交付保证 异常处理 动态 Topic 优化后的代码 这段代码展示了如何使用 Apache Flink 将数据流写入 Kafka,并提供了两种不同的 Kafka Sink 实现方式。以下是对代码的详细解析和说明: 代码结构 包声明:package sink


OpenClaw——让龙虾像真人一样控制桌面的SKILL(macOS版)
KD2026/4/19

一、背景 工作中要做一个桌面控制相关需求,试了下ClawHub现有Desktop Control skill,发现都有一些不好用的地方,或者与macOS系统不够适配,因此写了一个新skill供大家使用和交流 二、概述 这个Skill主要链路如下: 三、具体步骤实现拆解 1.初始化 这一步是最关键的,也是很多现有skill缺失的一步。第一版本先只做Retina屏兼容 在 macOS 上,即使截图和点击都用 Python,也仍然需要先确认几件事情: 截图图像尺寸是多少 屏幕逻辑尺寸是多少 鼠标


AI Agent 智能体开发入门:AutoGen 多智能体协作实战教程
Halcyon.平安2026/4/10

本文通过 AutoGen 框架,从单智能体到多智能体协作,循序渐进地讲解如何构建 AI Agent 系统,包含完整的代码示例和架构设计。 1. 多智能体协作架构 #mermaid-svg-TX83Bcl6adrsEqiY{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}


Claude Code 防上下文爆炸:源码级深度解析
lizhongxuan2026/4/2

基于 Claude Code v2.1.88 源码还原分析。本文从源码层面拆解 Claude Code 如何在长对话中管理上下文窗口,防止 token 爆炸,同时保持用户意图不被稀释。 问题:为什么上下文会爆炸? Claude Code 是一个 agentic coding 工具。一次典型的编码会话中,模型会: 读取十几个文件(每个几百到几千行) 执行 shell 命令并获取输出 搜索代码库(grep/glob 结果可能很大) 编辑文件并查看 diff 调用子 agent 处理子任务 每一


【万字长文】从 AI SDK 到 mini-opencode:一次很巧的 Go Agent 架构实践
mCell2026/3/25

同步更新至个人站点:从 AI SDK 到 mini-opencode:一次很巧的 Go Agent 架构实践 相关链接: 从零构建 Mini Claude Code:stack.mcell.top/blog/2026/a… 本次 Mini OpenCode 仓库地址:github.com/minorcell/m… Memo Code:memo.mcell.top/ 前阵子,我写过一篇 从零构建 Mini Claude Code 的 Agent 开发入门教程。 那次基本是顺着 AI SDK


Rust宏编程完全指南:用元编程解锁Rust的终极力量
土豆12502026/3/17

"宏就像是编译器的魔法棒,挥一挥,重复的代码就消失了。" —— 某位深夜 debug 的 Rustacean 目录 Why:为什么需要宏? What:宏是什么? How:如何使用宏? 声明宏 (macro_rules!) 派生宏 (Derive Macros) 属性宏 (Attribute Macros) 函数式宏 (Function-like Macros) 最佳实践 常见误区 总结 Why:为什么需要宏? 想象一下,你正在写一个 Web 框架,需要为 50 个不同的结构体实现相


【毕设】前后端(无模型训练)
2301_815389372026/3/8

后端 第一步,先建一个项目文件夹。 打开你电脑上任意一个地方,新建一个文件夹,就叫 ebike-detection,然后把你的 best.pt 复制进去。 第二步,安装Flask和相关依赖。 打开命令提示符(按 Win+R,输入 cmd,回车),然后把下面这行命令复制进去运行: pip install flask flask-cors ultralytics pillow 好,第三步,创建Flask后端文件。 在你的 ebike-detection


Node.js 安装与配置完全指南:从零开始搭建开发环境
张3蜂2026/2/28

目录 引言 第一部分:Node.js 简介与版本选择 1.1 什么是 Node.js? 1.2 Node.js 版本介绍 第二部分:Node.js 安装方式详解 2.1 方式一:官方安装包(最简单,适合初学者) Windows/macOS 安装步骤: 2.2 方式二:包管理器安装(适合 Linux 用户) Ubuntu/Debian 系统安装步骤: CentOS/RHEL 系统安装步骤: macOS 使用 Homebrew 安装: 2.3 方式三:使用 NVM 安装(最推

首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2026 聚合阅读