4. Java泛型与反射
问题背景
在 Java 后端面试中,泛型与反射几乎是必考的组合。面试官常问:泛型为什么在运行时会被擦除?反射如何获取泛型参数?如何动态创建带有泛型的对象? 理解二者的协同原理,不仅能应对面试,更能写出灵活、类型安全的框架代码。
核心概念
- 泛型(Generics):编译期类型检查机制,允许在定义类、接口、方法时使用类型参数,提供更强的类型安全并消除强制类型转换。
- 反射(Reflection):运行时获取类的完整结构(类名、方法、字段、注解、泛型信息等),并动态调用方法或创建对象。
- 类型擦除(Type Erasure):泛型信息只在编译期保留,编译后的字节码中会将泛型类型转换为原始类型(如
Object),同时编译器会插入桥接方法。 - Signature 属性:编译器在字节码的
Signature属性中保留泛型元数据,使得反射可以获取到完整的泛型类型。
原理机制
类型擦除 + Signature 属性 = 泛型反射的基础
- 编译期:泛型类型参数被擦除为边界类型(如
T extends Number擦除为Number),同时生成 桥方法 保持多态。 - 字节码:类、方法、字段的
Signature属性以字符串形式记录了完整的泛型签名,例如List<String>记录为Ljava/util/List<Ljava/lang/String;>。 - 运行时:反射 API(
Class.getGenericSuperclass()、Method.getGenericParameterTypes()等)解析Signature属性,返回ParameterizedType、TypeVariable等类型对象。
流程步骤:通过反射获取方法上的泛型参数类型
- 获取 Method 对象:调用
Class.getDeclaredMethod("methodName", parameterTypes)。 - 调用 getGenericParameterTypes():返回
Type[]数组,其中每个元素可能是ParameterizedType、TypeVariable等。 - 判断类型:如果类型是
ParameterizedType实例,则说明该参数使用了泛型(如List<String>)。 - 提取实际类型参数:通过
ParameterizedType.getActualTypeArguments()获取Type[],即为泛型参数的具体类型。 - 处理 TypeVariable:如果遇到
TypeVariable,则需要通过TypeVariable.getBounds()获取其边界信息。
// 示例:获取方法返回值的泛型类型
public <T> List<T> process(List<String> input) { ... }
Method method = MyClass.class.getDeclaredMethod("process", List.class);
Type returnType = method.getGenericReturnType();
if (returnType instanceof ParameterizedType) {
Type[] actualTypes = ((ParameterizedType) returnType).getActualTypeArguments();
System.out.println("返回值泛型类型: " + actualTypes[0]); // 实际为 TypeVariable T
}
案例说明
场景:实现一个通用 DAO 基类,通过反射获取实体类的泛型类型,避免手动传参。
// 抽象基类
public abstract class BaseDao<T> {
private Class<T> entityClass;
@SuppressWarnings("unchecked")
public BaseDao() {
// 获取当前子类的 Class,再获取其泛型父类的实际类型参数
ParameterizedType pt = (ParameterizedType) getClass().getGenericSuperclass();
entityClass = (Class<T>) pt.getActualTypeArguments()[0];
}
public T findById(Long id) {
// 使用 entityClass 进行 SQL 或 ORM 操作
// ...
}
}
// 子类使用
public class UserDao extends BaseDao<User> { } // 无需再指定 User.class
通过这种模式,子类会自动继承获取到正确的实体类型,避免了冗余的类型参数传递。
表格总结:泛型 vs 反射核心特性
| 维度 | 泛型 | 反射 |
|---|---|---|
| 阶段 | 编译期 | 运行时 |
| 核心目的 | 类型安全,消除强制转换 | 动态访问类结构,解耦 |
| 类型信息存储 | Signature 属性(保留元数据) | Class 对象、Type 接口体系 |
| 性能影响 | 几乎无运行时开销 | 有额外调用开销,JIT 可优化 |
| 常见应用 | 集合、函数式接口、自定义泛型类 | IoC 容器、动态代理、ORM 框架 |
误区提醒
- ⚠ 泛型不能用于直接 new 操作:例如
new T()是非法语句,必须通过反射clazz.newInstance()创建实例。 - ⚠ 反射获取的泛型类型可能是 TypeVariable:如果方法或类本身就有类型参数(如
<T>),则返回的是TypeVariable而非具体类,需要进一步绑定。 - ⚠ 桥方法干扰:编译器生成的桥方法也会被反射获取到,遍历方法时需通过
isBridge()进行过滤。 - ⚠ 静态上下文中无法获取泛型:
BaseDao<T>的静态方法无法通过getClass().getGenericSuperclass()获取类型参数。
实践清单
- 优先使用编译期泛型:能用泛型解决的问题尽量不用反射,保持类型安全和可读性。
- 缓存反射结果:如
Class.getDeclaredMethod()的结果应缓存到Map或静态变量中,避免频繁反射。 - 正确处理 ParameterizedType 嵌套:如
Map<String, List<Integer>>需要递归解析实际类型参数。 - 利用泛型工厂模式:结合
TypeReference或匿名内部类,手动保留泛型信息。 - 谨慎使用 getGenericSuperclass():确保当前类是直接继承泛型父类的具体子类,否则可能返回
Object。