什么是泛型?为什么要使用它?
什么是泛型?为什么要使用它?
zhangchangsheng面试考察点
- 基础概念掌握:面试官不仅仅是想知道泛型是什么,更是想知道你是否理解泛型的本质——参数化类型,以及它在编译期和运行期的不同表现。
- 类型安全意识:考察你是否清楚泛型如何解决类型安全问题,能否对比 “泛型前” 和 “泛型后” 的代码差异,理解为什么
ClassCastException从运行期提前到了编译期。 - 底层原理深度:考察你是否了解 类型擦除 机制,能否解释为什么
List<String>和List<Integer>在运行时是同一个类,以及泛型与 Java 向后兼容的设计权衡。
核心答案
Java 泛型(Generics) 是 JDK 5 引入的特性,允许在定义类、接口和方法时使用 类型参数,将类型从 “具体” 变成 “参数化”,从而在编译期进行类型检查。 **** **** **** ****
| 泛型核心价值 | 说明 |
|---|---|
| 类型安全 | 编译期检查类型,将运行时异常提前到编译期 |
| 消除强转 | 无需手动类型转换,代码更简洁 |
| 代码复用 | 一套代码适用于多种类型 |
| 设计优雅 | 泛型集合、泛型方法提升 API 设计质量 |
为什么要使用泛型?
| 使用泛型前 | 使用泛型后 |
|---|---|
| List list = new ArrayList();list.add(“hello”);String s = (String) list.get(0); // 需要强转 | List |
| list.add(123); // 编译通过,运行时报错 | list.add(123); // 编译直接报错 |
一句话总结:泛型通过编译期类型检查,保证了类型安全,消除了强制类型转换,让代码更健壮、更优雅。
深度解析
一、泛型的本质:参数化类型
// 传统方式:类型写死,每种类型都要定义一个类,大量重复代码 |
上图展示了泛型的核心思想——类型参数化。传统方式需要为每种类型单独定义一个类,导致大量重复代码;而泛型将类型作为参数传入,实现了一套代码适用于多种类型。
- 类型参数:如
<E>、<T>、<K, V>等,在定义时是占位符,在使用时被具体类型替换。 - 泛型类:类名后跟类型参数,如
ArrayList<E>。 - 泛型接口:接口名后跟类型参数,如
Comparable<T>。 - 泛型方法:方法返回值前声明类型参数,如
<T> T getFirst(List<T> list)。
二、泛型的三种使用方式
1. 泛型类
// 自定义泛型类 |
2. 泛型接口
// 泛型接口 |
3. 泛型方法
public class GenericMethodDemo { |
三、泛型的核心原理:类型擦除
// 编译前(源代码) |
上图展示了 Java 泛型的核心机制——类型擦除。这是理解泛型的关键。
- 擦除时机:编译期间,编译器将泛型类型替换为原始类型(Raw Type)或上界。
- 擦除规则:
- 无限界类型参数
<T>擦除为Object - 有上界
<T extends Number>擦除为Number - 多个上界
<T extends Serializable & Cloneable>擦除为第一个上界
- 无限界类型参数
- 运行时表现:
List<String> strings = new ArrayList<>(); |
类型擦除的代码对比:
// 源代码 |
四、通配符详解
| 通配符 | 含义 | 读 / 写 | 使用场景 |
|---|---|---|---|
<?> |
无界通配符,可持有任何类型 | 只能读,不能写(null 除外) | 不确定类型时的参数声明 |
<? extends T> |
上界通配符(协变),可以是 T 或 T 的子类 | 只能读,不能写(Producer Extends) | 从集合中读取数据 |
<? super T> |
下界通配符(逆变),可以是 T 或 T 的父类 | 只能写,不能读(Consumer Super) | 向集合中写入数据 |
记忆口诀:PECS(Producer Extends, Consumer Super)
上图对比了三种通配符的特点和使用场景,核心原则是 PECS(Producer Extends, Consumer Super)。 PECS 原则详解:
public class PECSDemo { |
五、泛型的限制
由于类型擦除,泛型有以下限制:
public class GenericLimitations<T> { |
六、泛型 vs Object 对比
| 对比维度 | 使用 Object | 使用泛型 |
|---|---|---|
| 类型安全 | 运行时才能发现类型错误 | 编译期就能发现类型错误 |
| 强制转换 | 必须手动强转 | 编译器自动插入强转 |
| 代码可读性 | 需要查看文档才知道类型 | 类型信息直观可见 |
| 性能 | 有装箱拆箱开销 | 基本类型需包装,但无额外开销 |
| 灵活性 | 灵活但不安全 | 安全且灵活 |
// Object 方式 |
面试高频追问
- 什么是类型擦除?为什么 Java 要这样设计? 类型擦除是编译器在编译时将泛型类型替换为原始类型或上界的过程。Java 这样设计是为了 向后兼容,确保 JDK 5 之前的代码在新版本中仍能运行,这是 Java 设计者的权衡之举。
List<?>、List<Object>和List有什么区别?List<?>:未知类型的 List,只能读不能写(类型安全)List<Object>:元素类型为 Object 的 List,可以读写任意对象List:原始类型,绕过泛型检查,不推荐使用
- 泛型数组为什么不能创建? 因为类型擦除后,
List<String>[]和List<Integer>[]都变成List[],如果允许创建,会导致类型安全问题。
常见面试变体
- “Java 泛型是如何实现的?和 C++ 模板有什么区别?”
- “什么是 PECS 原则?能举个例子吗?”
- “泛型的类型擦除会带来什么问题?如何解决?”
- “如何实现一个泛型数组?”
记忆口诀
泛型本质:类型参数化,编译期检查。 PECS 原则:生产者用 extends(只读),消费者用 super(只写)。 类型擦除:编译时擦除为 Object 或上界,运行时无泛型信息。
总结
Java 泛型通过类型参数化实现编译期类型检查,解决了类型安全和强制转换问题。核心原理是类型擦除——编译时将泛型类型替换为原始类型,这是 Java 为了向后兼容的设计权衡。使用时遵循 PECS 原则(Producer Extends, Consumer Super),并注意泛型的限制(不能用基本类型、不能创建泛型数组等)。
声明:本文转载自小哈的博客 《什么是泛型?为什么要使用它?》,版权归原文作者所有。转载请保留本声明与原文链接。
