面试拷打:Java 泛型 T / E / K / V / ?——只会用不会讲,答完面试官直接拒绝
👉 这是一个或许对你有用的社群
🐱 一对一交流/面试小册/简历优化/求职解惑,欢迎加入「芋道快速开发平台」知识星球。下面是星球提供的部分资料:
-
《项目实战(视频)》:从书中学,往事中“练”
-
《互联网高频面试题》:面朝简历学习,春暖花开
-
《架构 x 系统设计》:摧枯拉朽,掌控面试高频场景题
-
《精进 Java 学习指南》:系统学习,互联网主流技术栈
-
《必读 Java 源码专栏》:知其然,知其所以然

👉这是一个或许对你有用的开源项目
国产Star破10w的开源项目,前端包括管理后台、微信小程序,后端支持单体、微服务架构
RBAC权限、数据权限、SaaS多租户、商城、支付、工作流、大屏报表、ERP、CRM、AI大模型、IoT物联网等功能:
多模块:https://gitee.com/zhijiantianya/ruoyi-vue-pro
微服务:https://gitee.com/zhijiantianya/yudao-cloud
视频教程:https://doc.iocoder.cn
【国内首批】支持 JDK17/21+SpringBoot3、JDK8/11+Spring Boot2双版本
这道题面试官真正想筛什么
「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 个段位的标准答案给你——对照看看你卡在哪一档 。
基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
项目地址:https://github.com/YunaiV/ruoyi-vue-pro
视频教程:https://doc.iocoder.cn/video/
30 分答案:T / E / K / V 是什么
最简洁的标准答案——30 分天花板 :
T、E、K、V 这几个字母——语法上完全没区别 ——都是普通占位符,可以换成任何合法标识符。它们的差别在「社区约定」 ——大家约定俗成沿用久了就成了行业默契。
常见约定:
|
字母 |
全称 |
一般用在哪 |
|---|---|---|
| T |
Type |
通用类型,最常见 |
| E |
Element |
集合元素,如 |
| K |
Key |
Map 的键 |
| V |
Value |
Map 的值 |
| N |
Number |
数值类型 |
| S, U |
第二、第三个类型 |
多个类型参数时往后排 |
到这里 30 分天花板 ——能答出"语法没区别 + 社区约定"已经比一半同学强了,但面试官接下来就会追 :「那 ? 和 T 的区别是啥?什么时候用 ? extends T? 」——答不上 = 30 分以下 。
下面把最常见的三个带场景 给你过一下:
T——最通用的占位符
T 通常出现在你不知道(也不关心)具体类型 的场合——写工具类、写通用响应体,基本都用 T。
项目里最常见的实战 ——统一的 API 响应包装类:
public class ApiResponse<T> {
privateint code;
private String message;
private T data;
publicstatic <T> ApiResponse<T> success(T data) {
returnnew 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<>();
基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
项目地址:https://github.com/YunaiV/yudao-cloud
视频教程:https://doc.iocoder.cn/video/
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 件事:
-
把所有类型参数替换成原始类型 ——无界的
T换成Object,有上界的T extends Number换成Number; -
在读取泛型对象的地方,自动插入强转代码 ;
-
必要时生成桥接方法 ——保证继承关系里的多态正常工作。
所以运行时——**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>
、 |
| Map 的键值 | K, V |
Map<K, V> |
| 遍历未知类型的集合 | <?> |
printAll(List<?>) |
| 从集合中读取(生产者) | <? extends T> |
sum(List<? extends Number>) |
| 向集合中写入(消费者) | <? super T> |
addIntegers(List<? super Integer>) |
| 既读又写 |
固定类型 |
void process(List<T>) |
就一句话 :
-
T / E / K / V 是字母约定——语法没区别;
-
?是通配符——和 T 不是一回事; -
PECS 决定通配符方向——读 extends、写 super;
-
类型擦除 决定运行时限制——记住 3 个限制就够。
答到 30 分容易、答到 90 分得靠类型擦除 + 真踩过的坑 ——这就是面试时和别人拉开差距的地方。
欢迎加入我的知识星球,全面提升技术能力。
👉 加入方式,“长按”或“扫描”下方二维码噢:

星球的内容包括:项目实战、面试招聘、源码解析、学习路线。





文章有帮助的话,在看,转发吧。
谢谢支持哟 (*^__^*)
更多推荐
所有评论(0)