1004 字
5 分钟
Java 泛型、反射与动态代理:框架能力的基础

泛型帮助编译器检查类型,反射让程序在运行时检查和操作类型,代理则在原方法调用前后增加行为。Spring、MyBatis 等框架经常组合使用这些机制。

泛型有什么用?#

泛型把类型作为参数,例如 List<String>。编译器能阻止把 Integer 放进字符串列表,也减少取出元素后的强制类型转换。

Java 泛型具有不变性:List<Integer> 不是 List<Number> 的子类型。否则调用方可能把 Double 写进原本只保存 Integer 的列表,破坏类型安全。

什么是类型擦除?#

编译器主要在编译期检查泛型,生成字节码时把类型变量擦除为它的上界,未指定上界时通常为 Object,并按需插入类型转换等代码。

List<String> strings = new ArrayList<>();
List<Integer> numbers = new ArrayList<>();
System.out.println(strings.getClass() == numbers.getClass()); // true

它们不是两个不同的运行时 ArrayList 类。因此不能直接 new T(),也不能写 instanceof List<String>。但擦除不等于“泛型信息全部消失”:类、字段和方法声明中的泛型签名可保留在 class 元数据中,反射可能读取这些信息。

extends 与 super 怎么记?#

写法主要用途限制
List<? extends Number>从未知 Number 子类型的列表读取可以按 Number 读取,不能安全添加任意 Number 非 null 实例
List<? super Integer>向 Integer 或其父类型的列表写入 Integer可以添加 Integer,读取只能稳妥地按 Object 接收

这就是常说的 PECS:生产者用 extends,消费者用 super。它讨论的是类型安全,不是把列表变成只读或只写对象。

反射与注解有什么用?#

反射通过 Class、Method、Field 等对象检查类型、构造实例、调用方法或访问字段。框架可以根据配置和注解定位组件,而不把每个业务类写死在框架代码里。

注解本身主要提供元数据,必须由编译器、工具或运行时逻辑解释才会产生效果。需要运行时反射读取的注解,应使用 RUNTIME 保留策略。

反射的代价包括类型检查延后、访问限制和额外开销;现代 JDK 的模块及访问控制也会限制对内部成员的访问。基础面试知道用途和边界即可,不需要背所有反射 API。

动态代理是什么?#

代理对象接收调用,再在目标调用前后加上日志、事务、权限等行为。

调用方 → 代理对象 → 增强逻辑 → 目标方法 → 后续处理

JDK 动态代理基于接口,通过 InvocationHandler 处理调用。CGLIB 基于生成子类,对可重写的方法进行增强,因此不能通过子类重写方式代理 final 方法、private 方法,也不能继承 final 类。

静态代理需要事先编写代理类;动态代理在运行时生成代理类型或对象,减少重复代理代码。代理不等于原目标对象:绕过代理直接调用目标,就不会自动获得代理中的增强行为,这会影响 Spring 事务等功能。

面试回答模板#

泛型用于编译期类型安全,Java 通过类型擦除实现,运行时通常不会为不同类型参数生成不同的类。extends 适合读取生产者,super 适合写入消费者。反射用于运行时读取类型和调用成员,注解提供供框架解释的元数据。JDK 动态代理基于接口,CGLIB 基于子类,二者都通过代理调用加入增强逻辑。

自测#

  1. 泛型擦除后,能否通过反射读取字段声明的泛型参数?有时可以,声明签名可能仍保留。
  2. 为什么不能向 List<? extends Number> 随意添加 Integer?实际列表可能是 List<Double>。
  3. CGLIB 能增强 final 方法吗?不能通过子类重写这种方式增强。
Java 泛型、反射与动态代理:框架能力的基础
https://blog.xqcherry.top/posts/interview/java/java-generics-reflection-proxy/
作者
xqcherry
发布于
2026-10-12
许可协议
CC BY-NC-SA 4.0