👉 这是一个或许对你有用的社群

🐱 一对一交流/面试小册/简历优化/求职解惑,欢迎加入「芋道快速开发平台」知识星球。下面是星球提供的部分资料: 

👉这是一个或许对你有用的开源项目

国产Star破10w的开源项目,前端包括管理后台、微信小程序,后端支持单体、微服务架构

RBAC权限、数据权限、SaaS多租户、商城、支付、工作流、大屏报表、ERP、CRMAI大模型、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

集合元素,如 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> {
    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 件事:

  1. 把所有类型参数替换成原始类型 ——无界的 T 换成 Object,有上界的 T extends Number 换成 Number

  2. 在读取泛型对象的地方,自动插入强转代码 ;

  3. 必要时生成桥接方法 ——保证继承关系里的多态正常工作。

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

类型擦除带来 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 分得靠类型擦除 + 真踩过的坑 ——这就是面试时和别人拉开差距的地方。


欢迎加入我的知识星球,全面提升技术能力。

👉 加入方式,长按”或“扫描”下方二维码噢

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

文章有帮助的话,在看,转发吧。
谢谢支持哟 (*^__^*)

更多推荐