泛型帮助编译器检查类型,反射让程序在运行时检查和操作类型,代理则在原方法调用前后增加行为。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 基于子类,二者都通过代理调用加入增强逻辑。
自测
- 泛型擦除后,能否通过反射读取字段声明的泛型参数?有时可以,声明签名可能仍保留。
- 为什么不能向
List<? extends Number>随意添加 Integer?实际列表可能是List<Double>。 - CGLIB 能增强 final 方法吗?不能通过子类重写这种方式增强。