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

摘要: 原创出处 小哈学Java 「犬小哈」欢迎转载,保留摘要,谢谢!


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

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

这道题的隐藏考点

表面在问定义,实际面试官想看四件事:

  1. 你能不能不背书——用自己的话讲清楚”参数化类型”到底解决了什么问题。
  2. 你懂不懂类型擦除——这是 Java 泛型最反直觉的设计,也是各种诡异编译错误的根源。
  3. 你写代码用不用泛型——能不能把 List<String>、自定义 Box<T> 这类东西信手拈来,而不是只停留在概念层。
  4. 你踩没踩过坑——new T() 为什么不行?List<String> 能不能赋给 List<Object>?这些问题答得越利索,段位越高。

30 秒结论

Java 泛型 = 给类型加参数。 就像方法有形参,泛型让类、接口、方法也能接收”类型形参”,等到使用时再填入具体类型。

它解决三个痛点:

  • 类型安全:把 ClassCastException 从运行时拖到编译期——编译器帮你兜底,而不是用户帮你测 Bug。
  • 干掉强转:写 list.get(0) 直接拿到 String,不用再套 (String) 的壳。
  • 一套代码通吃多种类型:写一个 Box<T>,装 String、装 Integer 都行,不用复制粘贴出 StringBox、IntegerBox。

没有泛型的黑暗时代

Java 5 之前,集合里塞的全是 Object。一个 ArrayList 可以同时装 String 和 Integer,编译器完全不管,直到运行时一个 ClassCastException 把你炸醒:

List list = new ArrayList();
list.add("hello");
list.add(100); // 编译毫无怨言

String str = (String) list.get(0); // OK
String boom = (String) list.get(1); // 运行时爆炸:ClassCastException!

问题的根源:编译器对容器里装了什么一无所知,所有类型检查全靠程序员肉眼。

加上泛型之后,画风完全不同:

List<String> list = new ArrayList<>();
list.add("hello");
// list.add(100); // 编译直接报红,连运行的机会都不给

String str = list.get(0); // 无需强转,编译器已经帮你确认类型

一句话:泛型把”信任程序员”变成了”信任编译器”,后者显然更靠谱。

类型擦除:编译器对你撒的最大谎

Java 泛型是纯编译期魔法。编译器做完类型检查后,会把所有泛型信息一笔勾销——这就是类型擦除(Type Erasure)。

具体操作:把 T 替换成它的上界(没有就替换成 Object),然后在需要的位置插入强制转换。字节码里根本没有泛型的痕迹。

// 你写的                          // 编译后的字节码
List<String> list List list
list.get(0) (String) list.get(0)

List<String> 和 List<Integer> 在运行时是一回事——都是裸的 List。这就是为什么 list instanceof List<String> 编译不过,因为运行时根本不存在 List<String> 这个类型。

为什么这么设计? 一个字:兼容。Java 5 要保证老版本 .class 文件不用重新编译就能跑,所以只能把泛型做成编译期的”语法糖”。代价就是丢掉了运行时类型信息——这是历史包袱,不是最优解。

泛型三件套:类、方法、通配符

泛型类——最常见,集合框架到处都是:

public class Box<T> {
private T content;

public void setContent(T content) { this.content = content; }
public T getContent() { return content; }
}

Box<String> stringBox = new Box<>();
stringBox.setContent("Java");
String value = stringBox.getContent(); // 类型安全,零强转

泛型方法——类不需要泛型,但某个方法需要:

public static <E> void printArray(E[] array) {
for (E element : array) {
System.out.println(element);
}
}

printArray(new Integer[]{1, 2, 3}); // E 自动推断为 Integer
printArray(new String[]{"A", "B"}); // E 自动推断为 String

有界类型(Bounded Type)——给 T 划个范围:

public <T extends Number> double sum(List<T> list) {
return list.stream().mapToDouble(Number::doubleValue).sum();
}
// 只接受 Number 的子类,传个 List<String> 进来直接编译报错

通配符 ? 与 PECS 原则——泛型 API 设计的灵魂:

  • <? extends T>:只读,生产者(Producer Extends)。你能安全地从里面拿 T,但不能往里塞。
  • <? super T>:只写,消费者(Consumer Super)。你能安全地往里塞 T,但拿出来只能当 Object。

记住口诀:读用 extends,写用 super。

踩坑指南:这些写法编译器不买账

写法 能不能 原因
new T() 不行 类型擦除后 T 变成 Object,JVM 不知道要实例化哪个类
new T[] 不行 同上,数组需要在运行时知道元素的具体类型
list instanceof List<String> 不行 运行时泛型信息已被擦除
List<String> 赋给 List<Object> 不行 泛型不支持协变,否则类型安全就形同虚设
静态字段使用类的泛型参数 T 不行 静态成员属于类而非实例,T 要到实例化时才确定
原始类型 List 代替 List<?> 能编译,但别这么干 丢失所有泛型保护,编译器会发出 unchecked 警告

最后一条铁律:新代码永远不要用原始类型。 List 只是为了让 2004 年之前的代码还能跑,不是让你在 2026 年继续写。

文章目录
  1. 1. 这道题的隐藏考点
  2. 2. 30 秒结论
  3. 3. 没有泛型的黑暗时代
  4. 4. 类型擦除:编译器对你撒的最大谎
  5. 5. 泛型三件套:类、方法、通配符
  6. 6. 踩坑指南:这些写法编译器不买账