一、基础概念篇
- 什么是泛型?为什么 Java 要引入泛型? 泛型是 JDK 5 引入的参数化类型特性,允许将数据类型作为参数传递给类、接口、方法,使同一段代码可以复用在多种类型上。 引入泛型的核心价值:
- 编译期类型检查:提前发现类型不匹配问题,避免运行时抛出
ClassCastException - 消除强制类型转换:代码更简洁,减少转型出错概率
- 提升代码复用性:一套逻辑可适配多种数据类型,无需重复编写
- 增强可读性:容器等结构可以明确元素类型,降低理解成本
- 泛型有哪三种使用方式?分别举一个简单例子。
- 泛型类 :在类定义时声明泛型参数,如
public class Box<T> { private T data; } - 泛型接口 :在接口定义时声明泛型参数,如
public interface List<T> { boolean add(T e); } - 泛型方法 :在方法上独立声明泛型参数,与类的泛型无关,如
public <T> T getFirst(T[] array) { return array[0]; }注意:泛型方法的<T>必须写在修饰符之后、返回值之前。
- 泛型和使用 Object 相比,有什么优势? Object 是所有类的父类,也能实现 "通用",但存在两个核心问题:
- 没有编译期类型检查,存入什么类型都不会报错,取出时极易出现类型转换异常
- 每次取值都必须手动强转,代码冗余且易出错 泛型在编译期完成类型校验和自动转型,兼顾通用性与类型安全。
二、核心原理篇(高频核心,必问)
- 什么是泛型类型擦除?Java 泛型擦除的规则是什么? Java 的泛型是编译期伪泛型:泛型信息仅存在于编译阶段,编译完成后会将所有泛型参数替换为对应上界(无界则替换为 Object),并插入必要的强制转型和桥方法,运行时的字节码中不保留泛型的实际类型。 擦除规则:
- 无边界泛型(如
<T>):擦除为Object - 有上界泛型(如
<T extends Number>):擦除为上界类型Number - 通配符上界(如
? extends String):擦除为上界String - 通配符下界(如
? super Integer):擦除为Object
- 既然泛型会被擦除,那编译期是如何保证类型安全的? 编译期会在生成字节码之前,对泛型的类型使用进行严格校验:
- 写入操作:检查存入的元素类型是否与泛型声明一致,不一致直接编译报错
- 读取操作:自动在字节码中插入强制类型转换代码,无需开发者手动转型 类型擦除发生在编译校验之后,因此不会影响编译期的类型安全保障。
-
什么是桥方法(Bridge Method)?为什么会产生桥方法? 桥方法是编译器为了解决类型擦除后重写语义不匹配的问题,自动生成的合成方法。 产生原因:子类重写父类 / 接口的泛型方法时,擦除后子类方法的参数类型与父类擦除后的参数类型不一致,导致不构成重写。编译器会生成一个参数与父类擦除后一致的 "桥方法",内部调用子类真正的重写方法,保证多态语义成立。
class MyClass implements Comparable
{
// 开发者编写的重写方法
public int compareTo(MyClass o) { return 0; }
// 编译器自动生成的桥方法(擦除后接口方法参数为Object)
public int compareTo(Object o) { return compareTo((MyClass)o); }
}
三、通配符与边界篇(高频难点)
-
泛型通配符有哪几种?分别是什么含义? Java 泛型有三类通配符:
-
无界通配符
?:代表任意未知类型,适用于不依赖具体类型、仅使用 Object 方法的场景 -
上界通配符
? extends T:代表类型必须是 T 或 T 的子类,限定了类型的上限 -
下界通配符
? super T:代表类型必须是 T 或 T 的父类,限定了类型的下限 -
? extends T和? super T有什么核心区别?各自的读写特性是什么?
? extends T(上界) :- 可读:取出的元素可以安全赋值为 T 类型
- 不可写:无法添加除 null 外的任何元素 ------ 因为不知道?具体是哪个子类,无法保证写入类型安全
? super T(下界) :- 可写:可以安全添加 T 及其子类的对象 ------ 因为子类实例一定可以赋值给父类类型
- 读受限:取出的元素只能用 Object 接收 ------ 因为不知道?具体是哪个父类,无法安全向下转型
- 解释一下 PECS 原则,并说明使用场景。 PECS 是泛型通配符的使用准则,全称 Producer Extends, Consumer Super:
- 生产者(Producer) :如果集合是数据提供者(我们只从中读取元素),使用
? extends T - 消费者(Consumer) :如果集合是数据接收者(我们只往里面添加元素),使用
? super T - 如果同时需要读和写,不要使用通配符,直接使用具体泛型类型。 典型案例:
Collections.copy(dest, src)方法,src 是生产者用? extends T,dest 是消费者用? super T。
List<?>和List(原生类型)有什么区别?
List是原始类型,完全放弃泛型检查,可以添加任意类型元素,编译只会给出警告,极易造成类型安全问题List<?>是无界通配符,代表 "未知类型的列表",编译期会做安全检查:不能添加除 null 外的任何元素,读取只能得到 Object,是类型安全的 简单说:原生类型是 "放弃类型安全",无界通配符是 "不知道具体类型,但遵守类型安全"。
- 泛型是不变的,数组是协变的,这句话怎么理解?
- 数组协变 :如果 A 是 B 的子类,那么
A[]是B[]的子类,可以向上转型。例如String[]可以赋值给Object[],但运行时会有数组存储检查,放入错误类型会抛ArrayStoreException - 泛型不变 :即使 A 是 B 的子类,
List<A>也不是List<B>的子类,不能向上转型。例如List<String>不能赋值给List<Object>,编译直接报错 这也是 Java 不允许创建泛型数组的核心原因之一 ------ 二者类型特性冲突,会破坏泛型的类型安全。
四、限制与避坑篇(高频细节题)
-
为什么泛型不能直接使用基本数据类型,必须用包装类? 类型擦除后,泛型会被替换为 Object 或其上界,而基本数据类型不是 Object 的子类,无法完成替换。因此泛型只能使用引用类型,基本类型需要通过自动装箱拆箱机制配合包装类使用。
-
为什么不能直接
new T()创建泛型实例?有什么替代方案? 类型擦除后 T 变成了 Object 或上界类型,运行时不知道 T 的具体类型,因此无法直接实例化。 替代方案: -
通过反射创建:传入
Class<T> clazz,调用clazz.newInstance()或构造器 newInstance -
使用工厂方法 / 提供者模式,由外部传入创建逻辑
-
借助 TypeToken 等工具在运行时获取实际类型后反射创建
-
为什么 Java 不允许创建泛型数组(
new T[10])? 核心原因是数组协变与泛型不变的特性冲突,会破坏类型安全: -
数组是协变的,
T[]擦除后变为Object[],可以向上转型为Object[] -
如果允许创建泛型数组,就可以往
Object[]中放入任意类型对象,而数组本身不会报错(因为运行时数组类型就是 Object) -
这会绕过泛型的编译期检查,导致运行时出现类型转换异常,破坏泛型的安全承诺 因此 Java 从语法层面禁止直接创建泛型数组。
-
可以用
instanceof判断泛型类型吗?为什么? 不可以。instanceof是运行时操作,而泛型在编译后已经被擦除,运行时不存在具体的泛型类型信息。例如obj instanceof List<String>会直接编译报错,只能判断obj instanceof List(原始类型)。 -
静态方法 / 静态变量为什么不能使用类定义的泛型参数? 类的泛型参数需要在实例化对象时才确定具体类型,而静态成员属于类本身,不依赖对象实例,在类加载时就已经存在。此时泛型类型还未确定,因此静态成员无法使用类的泛型参数。 注意:静态方法可以定义自己的泛型,成为静态泛型方法。
-
异常类可以定义为泛型吗?为什么? 不可以。异常是在运行时抛出并捕获的,而泛型会被类型擦除,无法在运行时区分不同泛型参数的异常类型,也无法实现按泛型类型分别捕获,因此 Java 语法规定
Throwable的子类不能是泛型类。
五、进阶实战篇(中高级面试)
-
运行时能不能获取泛型的实际类型?怎么获取? 普通的泛型实例在运行时无法获取类型(已被擦除),但类、方法、字段定义上的泛型信息会保留在字节码中,可以通过反射获取:
-
获取类的泛型父类:
getGenericSuperclass()得到ParameterizedType,再调用getActualTypeArguments()获取实际类型参数 -
获取方法参数 / 返回值、字段的泛型:对应
getGenericParameterTypes()、getGenericReturnType()、getGenericType() -
运行时获取实例泛型:可通过 TypeToken(如 Guava、Gson 中的实现),原理是利用匿名内部类在编译时保留泛型信息,通过反射读取。
-
什么是堆污染(Heap Pollution)?什么场景会出现?如何避免? 堆污染指一个泛型变量指向了一个不符合其泛型类型的对象,导致编译期类型检查失效,运行时可能抛出
ClassCastException。 常见触发场景:
- 原始类型与泛型类型混用赋值
- 泛型可变参数(
T... args)与数组结合 - 未经检查的强制类型转换 避免方式:
- 尽量不混用原始类型和泛型
- 避免泛型可变参数与数组操作结合
- 合理使用
@SafeVarargs声明安全的可变参数方法 - 减少不必要的 unchecked 强转
@SuppressWarnings("unchecked")注解的作用是什么?什么时候使用? 该注解用于抑制编译器的 "未检查类型转换" 警告,通常出现在:
- 泛型强制类型转换,且开发者确认该转换是类型安全的场景
- 调用遗留的非泛型 API,不可避免产生 unchecked 警告时 注意:不能滥用,必须在确认类型安全的前提下使用,避免掩盖真实的类型风险。
- 泛型方法的类型参数和泛型类的类型参数是什么关系?会冲突吗? 二者是完全独立的,泛型方法的类型参数作用域仅限方法内部。如果泛型方法声明了和类同名的泛型参数(如类是
Box<T>,方法也声明<T>),方法内的 T 会覆盖类的 T,属于局部变量遮蔽,不推荐这样写,容易造成混淆。