前言
写 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 泛型的定义
泛型就是"参数化类型"——把类型当作参数,让同一套代码能处理不同类型的数据。
一句话理解:泛型就是把"类型"从写死的具体类,变成可以传的参数——就像方法可以传值参数一样,泛型让类/接口/方法可以传"类型参数"。
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 类型参数的命名惯例
| 字母 | 含义 | 常见场景 |
|---|---|---|
| E | Element | 集合元素(如 List<E>) |
| K | Key | 键(如 Map<K, V>) |
| V | Value | 值(如 Map<K, V>) |
| T | Type | 通用类型 |
| 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 T | T 或其子类 | 能 | 不能(除 null) | T | 无 |
| ? super T | T 或其父类 | 能 | 能 | Object | T 及其子类 |
| <T>(确定类型) | 具体的 T | 能 | 能 | T | T |
四、场景:泛型的典型应用
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
类型擦除后,MyPair 的 setFirst 变成了 setFirst(Object)。而 DatePair 只有 setFirst(Date)。按理说 JVM 找不到重写方法,多态失效。
但编译器会在 DatePair 中自动生成一个桥方法:
1// 编译器生成的桥方法(你看不到,但确实存在) 2public void setFirst(Object value) { 3 setFirst((Date) value); // 强制类型转换,调用你写的那个方法 4} 5
这就是为什么父类引用指向子类对象时,调用 setFirst(Object) 依然能正确执行子类逻辑的原因。
考点四:泛型的限制(常考)
面试官:泛型有哪些限制?为什么不能 new T()?为什么不能创建泛型数组?
参考回答(都因为类型擦除):
- 不能实例化类型参数:
new T()非法,擦除后变成了new Object(),毫无意义。通常通过传入Class<T>并反射实例化 - 不能是基本类型:
List<int>错误,必须是List<Integer>,因为擦除后是 Object,而基本类型不是 Object 的子类 - 不能创建泛型数组:
new T[]或new List<String>[]非法,因为数组是协变的,且在运行时必须知道确切的组件类型,这与泛型擦除冲突,会破坏类型安全 - 不能用于 instanceof 检查:
if (obj instanceof List<String>)非法,因为运行时只有List。正确写法是if (obj instanceof List) - 静态成员不能用类的泛型参数:静态变量和方法属于类,在实例化之前就已存在,此时 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学习笔记之泛型》 是转载文章,点击查看原文。

