⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 http://www.iocoder.cn/Fight/JavafanxingTEKVnihaizaishashafenbuqing/ 「芋道源码」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

这道题面试官真正想筛什么

「Java 泛型这几个字母怎么用」是 Java 中级岗的高频面试题——表面上是问语法,实际上面试官在筛 3 件事:

  • 基础语法层:T、E、K、V 之间是不是真有语法区别?通配符 ? 和类型参数 T 是不是同一回事?——答不出区别 = 没看过源码;
  • 设计取舍层:什么时候用 ? extends、什么时候用 ? super?为什么 PECS?——答不上 PECS = 写代码全靠抄;
  • JVM 层:泛型在运行时还在吗?为什么 T[] arr = new T[10] 编译报错?——答出类型擦除 = 真懂 JDK。

我自己面试过的人里——80% 卡在第二档,会用泛型但讲不清通配符 + PECS。下面把 30 分 / 60 分 / 90 分 3 个段位的标准答案给你——对照看看你卡在哪一档。

30 分答案:T / E / K / V 是什么

最简洁的标准答案——30 分天花板:

T、E、K、V 这几个字母——语法上完全没区别——都是普通占位符,可以换成任何合法标识符。它们的差别在「社区约定」——大家约定俗成沿用久了就成了行业默契。

常见约定:

字母 全称 一般用在哪
T Type 通用类型,最常见
E Element 集合元素,如 List<E>
K Key Map 的键
V Value Map 的值
N Number 数值类型
S, U 第二、第三个类型 多个类型参数时往后排

到这里 30 分天花板——能答出”语法没区别 + 社区约定”已经比一半同学强了,但面试官接下来就会追:「那 ? 和 T 的区别是啥?什么时候用 ? extends T?」——答不上 = 30 分以下。

下面把最常见的三个带场景给你过一下:

T——最通用的占位符

T 通常出现在你不知道(也不关心)具体类型的场合——写工具类、写通用响应体,基本都用 T。

项目里最常见的实战——统一的 API 响应包装类:

public class ApiResponse<T> {
private int code;
private String message;
private T data;

public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>(200, "成功", data);
}
}

public ApiResponse<User> getUserById(Long id) { ... }
public ApiResponse<List<Product>> getFeaturedProducts() { ... }

返回用户信息 → T 是 User;返回商品列表 → T 是 List<Product>——一份模板复用整个项目。

E——集合的元素

JDK 里 ArrayList<E> / HashSet<E> 都是这个风格。自己写一个通用树节点(组织架构 / 分类目录 / 评论嵌套——业务里非常常见):

public class TreeNode<E> {
private E data;
private List<TreeNode<E>> children;

public void addChild(TreeNode<E> child) {
if (children == null) children = new ArrayList<>();
children.add(child);
}
}

E 换成 Department / Category / Comment 都没问题。

K 和 V——键值对

HashMap<K, V> 就是这写法。写一个本地缓存:

public class LocalCache<K, V> {
private final Map<K, V> cache = new ConcurrentHashMap<>();

public void put(K key, V value) { cache.put(key, value); }
public V get(K key) { return cache.get(key); }
}

LocalCache<Long, User> userCache = new LocalCache<>();
LocalCache<String, List<Product>> categoryCache = new LocalCache<>();

60 分答案:通配符 ? 和 PECS 原则

要进 60 分门槛——必须答对两件事:? 和 T 的本质区别 + PECS 原则。

? 和 T 的本质区别

T、E、K、V 是类型参数——你自己定义的占位符,使用时会被具体类型替换。

? 是通配符——表示”某个我不确定的类型”——一般出现在方法参数、变量声明里。两者不能混用:

  • <T>:先声明、再使用——public <T> void foo(T arg) 这里 T 是先声明的;
  • <?>:直接使用、不声明——public void foo(List<?> arg) 这里 ? 没有声明动作。

3 种通配符——读 / 写权限的核心差异

public static void printAll(List<?> list) {           // 无界:什么都能传
for (Object element : list) { ... }
}

public static double sum(List<? extends Number> list) { // 上界:只读
double total = 0;
for (Number n : list) { total += n.doubleValue(); }
return total;
}

public static void addIntegers(List<? super Integer> list) { // 下界:只写
list.add(10);
list.add(20);
}

3 种通配符对比表:

通配符 能读吗 能写吗 适合场景
<?> 读出来是 Object ❌ 不能(null 除外) 只遍历,不关心类型
<? extends T> 读出来是 T ❌ 不能 只读,生产数据
<? super T> 读出来是 Object ✅ 能写 T 及子类 只写,消费数据

PECS 原则——决定通配符方向的唯一口诀

PECS 是 Joshua Bloch 在《Effective Java》里提的——全称 Producer Extends, Consumer Super:

  • 从集合里读数据(生产者)→ 用 ? extends T;
  • 往集合里写数据(消费者)→ 用 ? super T。

JDK 里 Collections.copy 就是教科书级例子:

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));
}
}
  • src 是来源——从里面读数据——用 ? extends T(生产者);
  • dest 是目标——往里面写数据——用 ? super T(消费者)。

这个方法可以把 List<Integer> 复制到 List<Number> 里——非常灵活。

如果两个都需要(既读又写)——别用通配符,直接用固定的类型参数 T。

到这里答完——拿 60 分稳了。但还差最后那一刀——类型擦除 + 实战避坑。

90 分答案:类型擦除 + 实战避坑

90 分的差异化在两点——为什么会有限制 + 怎么绕过这些限制。

类型擦除——编译完之后泛型其实”消失了”

有一个问题很多人没想过——泛型信息在运行时还在吗?

答案:不在了。

Java 的泛型是编译期生效——编译完成后字节码里没有 T、E 这些东西。这个过程叫类型擦除(Type Erasure)。

编译器干 3 件事:

  1. 把所有类型参数替换成原始类型——无界的 T 换成 Object,有上界的 T extends Number 换成 Number;
  2. 在读取泛型对象的地方,自动插入强转代码;
  3. 必要时生成桥接方法——保证继承关系里的多态正常工作。

所以运行时——List<String> 和 List<Integer> 其实是同一个类:ArrayList.class。getClass() 打印两个结果一模一样。

类型擦除带来 3 个写代码时要注意的限制:

// 限制 1:不能用 instanceof 判断具体的泛型类型
if (list instanceof List<String>) { ... } // ❌ 编译报错
if (list instanceof List<?>) { ... } // ✅ 只能这样

// 限制 2:不能直接 new 一个泛型类型
T obj = new T(); // ❌ 不行
T obj = clazz.getDeclaredConstructor().newInstance(); // ✅ 反射绕过

// 限制 3:不能创建泛型数组
T[] arr = new T[10]; // ❌ 编译报错
T[] arr = (T[]) Array.newInstance(clazz, 10); // ✅ 反射绕过

答出类型擦除 + 3 个限制 + 怎么绕过——这是 90 分的关键差异化。

我自己踩过的一个真坑

5 年前我写过这样一段代码——当时觉得自己很高级:

public static void copyAll(List<? extends Object> src, List<? super Object> dst) {
for (int i = 0; i < src.size(); i++) {
dst.set(i, src.get(i));
}
}
  • ? extends Object 看起来像”上界”——实际上 Object 是所有类的父类、上界等于没界——和 <?> 完全等价(src 退化成 List<?>——只能读出 Object,不能 add 任何元素);
  • ? super Object 看起来像”下界”——Object 没有父类——这条直接就是 List<Object>(dst 就是 List<Object>——能写任意对象进去)。

整段代码 = List<?> 当 src + List<Object> 当 dst——通配符全是无效装饰、写法等价于 void copyAll(List<?> src, List<Object> dst)。这就是写 10 年 Java 也未必”用对”泛型的典型表现——语法层面会用,但究竟该用 T 还是 ?、什么时候 extends 什么时候 super——大多数人都答不全。

面试官如果丢这种代码让你 review——你能不能一眼看出”通配符全是装饰”?这就是 90 分和 60 分的差距。

直接掉分的 4 种答法

按踩到概率从高到低:

  • ❌ 「T 和 E 是不一样的类型」 —— 直接进 reject 池。T、E 在语法上完全等价——这是 30 分的边界,答错就是基础不牢;
  • ❌ 「? 和 T 都是泛型占位符,没区别」 —— 当场扣 30 分。T 是先声明再使用、? 是直接使用不声明——本质不同;
  • ❌ 「List<?> 可以随便 add」 —— 这答案直接证明你没真用过通配符——List<?> 除了 null 外什么都不能 add;
  • ❌ 「泛型在运行时也是 List<String>」 —— 错——这答案直接说”我没看过 JVM 字节码”——运行时全是 ArrayList.class,类型擦除掉了。

高频追问怎么接

追问 1:<T extends Number & Comparable<T>> 这种多重边界什么时候用?

要求 T 同时是 Number 子类且实现 Comparable——类必须放在前面、接口在后。实战中很少用——除非写算法库(如 Collections.max(Collection<T>))。

追问 2:泛型类的静态方法不能用类的类型参数?

对。class Box<T> 里的 static T staticMethod() 会编译报错——因为静态方法和类实例无关、T 在静态上下文不存在。需要的话给静态方法单独声明类型参数 static <T> T staticMethod()。

追问 3:为什么 List<Integer> 不能赋值给 List<Number>?

因为泛型不支持协变(covariance)。Integer extends Number 但 List<Integer> 不 extends List<Number>——否则你能写 List<Number> nums = list; nums.add(3.14);——直接污染 List<Integer>。

追问 4:能不能写 T extends Comparable<? super T>?

经典写法——比 T extends Comparable<T> 更宽松。例子:

class Person implements Comparable<Person> {
int age;
public int compareTo(Person other) { return this.age - other.age; }
}

class Student extends Person { /* 继承 compareTo */ }

如果方法签名写成 <T extends Comparable<T>> void sort(List<T>)——sort(List<Student>) 编译报错(Student 没有 Comparable<Student>、只有继承自 Person 的 Comparable<Person>)。

写成 <T extends Comparable<? super T>> 就 OK——Student 的父类 Person 实现了 Comparable,满足”T 或 T 的父类有 Comparable”——sort(List<Student>) 直接通过。JDK 里 Collections.sort 就是这写法——为了支持子类复用父类的排序逻辑。

一张速查表收尾

最后给一张可以直接收藏的速查表——遇到不确定的回来查一下:

场景 用什么 例子
不知道具体类型,需要类型安全 T <T> ApiResponse<T>
集合中的元素类型 E List<E>、TreeNode<E>
Map 的键值 K, V Map<K, V>
遍历未知类型的集合 <?> printAll(List<?>)
从集合中读取(生产者) <? extends T> sum(List<? extends Number>)
向集合中写入(消费者) <? super T> addIntegers(List<? super Integer>)
既读又写 固定类型 T void process(List<T>)

就一句话:

  • T / E / K / V 是字母约定——语法没区别;
  • ? 是通配符——和 T 不是一回事;
  • PECS 决定通配符方向——读 extends、写 super;
  • 类型擦除 决定运行时限制——记住 3 个限制就够。

答到 30 分容易、答到 90 分得靠类型擦除 + 真踩过的坑——这就是面试时和别人拉开差距的地方。

文章目录
  1. 1. 这道题面试官真正想筛什么
  2. 2. 30 分答案:T / E / K / V 是什么
    1. 2.0.1. T——最通用的占位符
    2. 2.0.2. E——集合的元素
    3. 2.0.3. K 和 V——键值对
  • 3. 60 分答案:通配符 ? 和 PECS 原则
    1. 3.1. ? 和 T 的本质区别
    2. 3.2. 3 种通配符——读 / 写权限的核心差异
    3. 3.3. PECS 原则——决定通配符方向的唯一口诀
  • 4. 90 分答案:类型擦除 + 实战避坑
    1. 4.1. 类型擦除——编译完之后泛型其实”消失了”
    2. 4.2. 我自己踩过的一个真坑
  • 5. 直接掉分的 4 种答法
  • 6. 高频追问怎么接
    1. 6.0.1. 追问 1:<T extends Number & Comparable<T>> 这种多重边界什么时候用?
    2. 6.0.2. 追问 2:泛型类的静态方法不能用类的类型参数?
    3. 6.0.3. 追问 3:为什么 List<Integer> 不能赋值给 List<Number>?
    4. 6.0.4. 追问 4:能不能写 T extends Comparable<? super T>?
  • 7. 一张速查表收尾