一、注解
1.1 概述
Java 注解(Annotation)又称 Java 标注,是 JDK 5.0 引入的一种注释机制。注解是元数据的一种形式,提供有关于程序但不属于程序本身的数据。因此注解对其注解的代码没有直接影响。
1.2 注解声明
Java 中所有的注解,默认实现 Annotation 接口:
package java.lang.annotation; |
注解的声明使用 @interface 关键字:
public XXXXX { |
1.3 元注解
在定义注解时,注解类也能够使用其他注解声明。对注解类型进行注解的注解类,亦即注解上面的注解,我们称之为”meta-annotation”(元注解)。一般需要指定的元注解有两个:
@Target
注解标记另一个注解,从而限制注解的 Java 元素类型:
- ElementType.ANNOTATION_TYPE — 应用注解类型
- ElementType.CONSTRUCTOR — 应用构造函数
- ElementType.FIELD — 应用字段或属性
- ElementType.LOCAL_VARIABLE — 应用局部变量
- ElementType.METHOD — 应用于方法
- ElementType.PACKAGE — 应用于包声明
- ElementType.PARAMETER — 应用于方法的参数
- ElementType.TYPE — 应用于类的任何元素(只能注解类、接口、枚举)
@Retention
注解指定标记注解保留方式:
- RetentionPolicy.SOURCE — 标记的注解仅保留在源级别中,并被编译器忽略
- RetentionPolicy.CLASS — 标记的注解在编译时由编译器保留,但 JVM 会忽略
- RetentionPolicy.RUNTIME — 标记的注解由 JVM 保留,因此运行时环境可以使用
1.4 注解类型元素
在元注解中,允许使用注解时传递参数,同时也能让自定义注解的主体包含 annotation type element(注解类型元素):
|
注意:在使用注解时,如果定义的注解中的类型元素无默认值,则必须进行传值。
// 如果只存在 value 元素需要传值,则可以省略元素名 |
1.5 注解应用场景
RUNTIME
注解保留至运行期,意味着我们能够在运行期间结合反射技术获取注解中的所有信息。
CLASS
注解会保留在 class 文件中,但是会被虚拟机忽略(即无法在运行期反射获取注解)。这种注解的应用场景为字节码操作,如:AspectJ、热修复 Robust。
如果我们使用普通的编程方式,需要在代码中疯狂进行 if-else 判断,如果存在十处就需要在这十个判断点加入校验判断。此时,我们可以借助 AOP(面向切面) 编程思想,将程序中所有功能点划分为:需要登录和无需登录两种类型,即两个切面。对于切面的切分即可采用注解。
|
在操作字节码时,就能够根据方法是否具备该注解来修改 class 中该方法的内容加入 if-else 的代码段:
// Class 字节码被修改后 |
SOURCE
IDE 语法检查:在 Android 开发中,support-annotations 与 androidx.annotation 中均有提供 @IntDef 注解:
|
Java 中 Enum(枚举)的实质是特殊单例的静态成员变量,在运行期所有枚举类作为单例全部加载到内存中,比常量多 5 到 10 倍的内存占用。此注解的意义在于能够取代枚举,实现如方法入参限制。
APT 注解处理器:APT 全称为 “Annotation Processor Tools”,意为注解处理器。编写好的 Java 源文件需要经过 javac 的编译,翻译为虚拟机能够加载解析的字节码 Class 文件。注解处理器是 javac 自带的一个工具,用来在编译时期扫描处理注解信息。你可以为某些注解注册自己的注解处理器。
注解处理器是对注解应用最为广泛的场景,在 Glide、EventBus3、ButterKnife、Tinker、ARouter 等常用框架中都有注解处理器的身影。你可能发现这些框架中对注解的定义并不是 SOURCE 级别,更多的是 CLASS 级别。CLASS 包含了 SOURCE,RUNTIME 包含 SOURCE、CLASS。
二、反射
2.1 概述
一般情况下,我们使用的某个类时必定知道它是什么类,是用来做什么的,并且能够获得此类的引用。于是我们直接对这个类进行实例化,之后使用这个类对象进行操作。
而反射不一样,它应用于一开始并不知道自己要初始化的类对象是什么情形下,自然也无法使用 new 关键字来创建对象。此时,我们可以使用 JDK 提供的反射 API 进行反射调用。反射就是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性,并且能够改变它的属性。 这也是 Java 被视为动态语言的关键。
Java 反射机制提供了以下功能:
- 在运行时构造任意一个类的对象
- 在运行时获取或者修改任意一个类所具有的成员变量和方法
- 在运行时调用任意一个对象的方法
2.2 Class
反射始于 Class,Class 是一个类,封装了当前对象所对应的类的信息。 Class 类是一个对象镜面映射出的结果,对象可以在 Class 这面镜子里看到自己的属性、方法、构造器,实现了哪些接口等等,即使是私有的,藏在内心处的,也可以通过手段查到。对于每个类而言,JRE 都为其保留了一个不变的 Class 类型的对象,同样,一个类在 JVM 中只会有一个 Class 类。
获得 Class 对象
获取 Class 对象的三种方法:
- 通过类名获取:
类名.class - 通过对象获取:
对象名.getClass() - 通过全类名获取:
Class.forName(全类名)或classLoader.loadClass(全类名)
Class.forName vs ClassLoader.loadClass
// Class.forName: 加载 + 链接 + 初始化(执行 static 代码块) |
创建实例
通过反射来生成对象主要有两种方式:
- 使用 Class 对象的 newInstance() 方法来创建 Class 对象对应类的实例:
Class<?> cls = Integer.class; |
- 先通过 Class 对象获取指定的 Constructor 对象,再调用 Constructor 对象的 newInstance 方法来构建对象:
Class<?> cls = String.class; |
获取构造器信息
Constructor getConstructor(Class[] params) — 获得指定参数类型的 public 构造函数(包括父类) |
获取类的成员变量(字段)信息
Field getField(String name) — 获得指定名称的 public 字段 |
调用方法
Method getMethod(String name, Class[] params) — 使用特定的参数类型,获得命名的 public 方法 |
当我们从类中获取一个方法之后,就可以使用 invoke() 方法来调用这个方法:
public Object invoke(Object obj, Object... args) |
2.3 setAccessible 与安全检查
Method method = SomeClass.class.getDeclaredMethod("privateMethod"); |
在 Java 9+ 中,模块系统引入了严格的封装限制。反射访问内部 API 可能触发 InaccessibleObjectException,需要通过 --add-opens JVM 参数显式开放。
三、利用反射创建数组
数组在 Java 里是比较特殊的一种类型,它可以赋值给一个 Object Reference,其中的 Array 类为 java.lang.reflect.Array 类。通过 Array.newInstance() 创建数组对象:
public static Object newInstance(Class<?> componentType, int length); |
// 通过反射创建数组示例 |
四、反射获取泛型的真实类型
当我们对一个泛型类进行反射时,需要得到泛型中的真实数据类型,来完成比如 json 反序列化的操作。此时需要通过 Type 体系来完成。Type 接口包含了一个实现类(Class)和四个实现接口:
- TypeVariable:泛型类型变量。可以获取泛型上下限等信息。
- ParameterizedType:具体的泛型类型,可以获得元数据中泛型签名类型(泛型真实类型)。
- GenericArrayType:当需要描述的类型是泛型类的数组时,比如 List[],Map[],此接口会作为 Type 的实现。
- WildcardType:通配符泛型,获得上下限信息。
TypeVariable
public class TestType <K extends Comparable & Serializable, V> { |
ParameterizedType
public class TestType { |
GenericArrayType
public class TestType<T> { |
WildcardType
public class TestType { |
五、反射的性能开销与优化
5.1 反射性能开销的来源
反射比直接调用慢,主要开销来自:
- 类型安全检查:每次调用 invoke 都需要检查访问权限、参数类型匹配。
- 参数装箱/拆箱:invoke 的参数和返回值都是 Object 类型,基本类型需要装箱/拆箱。
- 方法内联限制:JIT 编译器无法内联反射调用的方法。
- 可见性检查:setAccessible 绕过访问控制,但 JVM 仍需要验证。
5.2 Inflation 机制
// JVM 对反射有优化——Inflation(膨胀)机制 |
5.3 反射优化最佳实践
// 1. 缓存 Method/Field/Constructor 对象 |
六、Android 中的反射限制
6.1 Android 隐藏 API 限制
从 Android 9.0 (API 28) 开始,Google 引入了对隐藏 API(非 SDK 接口)的访问限制:
API 级别分类: |
// 访问隐藏 API 时的典型异常 |
6.2 绕过隐藏 API 限制的技术
// 方法 1: 使用元反射(meta-reflection)——反射"反射本身" |
七、实战:Gson 反序列化
static class Response<T> { |
在进行 Gson 反序列化时,存在泛型时,可以借助 TypeToken 获取 Type 以完成类型的反序列化。为什么 TypeToken 要被定义成抽象类呢? 因为只有定义为抽象类或者接口,才能在使用中,对需要的实体类进行相对应的创建,此时确定泛型类型,编译才能够将泛型的签名信息正确地记录到 Class 元数据中——即通过 getClass().getGenericSuperclass() 获取父类(TypeToken<Response>)的 ParameterizedType,从中提取 Response<Data> 的实际类型实参。
八、动态代理
Java 动态代理是反射的重要应用之一:
// JDK 动态代理 |
九、面试常问题目
Q1: Class.forName 和 ClassLoader.loadClass 有什么区别?
Class.forName 默认会完成类的加载、链接和初始化(执行 static 代码块),也可以指定 initialize=false 跳初始化。ClassLoader.loadClass 只加载类,不进行初始化和链接(在首次使用时才初始化)。典型应用:JDBC 驱动加载使用 Class.forName 触发 static 代码块注册驱动。在需要延迟类初始化的场景(如 Spring 的懒加载)使用 loadClass。
Q2: 反射的性能为什么差?有什么优化手段?
反射性能开销来自:(1) 需要做类型安全检查和方法查找;(2) 参数和返回值需要装箱/拆箱;(3) JIT 无法内联反射调用;(4) 首次反射调用需要 JNI 查找。优化手段:(1) 缓存 Method/Field/Constructor 对象;(2) 调用 setAccessible(true) 禁用安全检查;(3) 利用 JVM 的 Inflation 机制(15 次后自动生成字节码加速);(4) 使用 MethodHandle 或 LambdaMetafactory 替代频繁的反射调用;(5) 对于已知类型的频繁调用,将 MethodHandle 转为 Lambda,性能接近直接调用。
Q3: Android 的隐藏 API 限制是什么?如何实现双反射绕过?
从 Android 9.0 开始,Google 限制了对隐藏 API(@hide 标记的 SDK 内部接口)的访问。通过反射访问受限 API 时会抛出 NoSuchMethodException 或 NoSuchFieldException。双反射(Dual Reflection)的绕过原理:先反射获取 Class.getDeclaredMethod 等底层方法本身,然后通过获取到的”元方法”来搜索受限方法——由于 ART 层面的检查在某些版本中有漏洞,使用元反射可以绕过。但这种方法在每个 Android 新版本中可能失效,Google 在持续加固限制。
Q4: 什么是反射的 Inflation 机制?它是如何提高反射性能的?
Inflation 是 JVM 对反射调用的优化。前 15 次反射调用使用 NativeMethodAccessor(通过 JNI 调用),速度较慢。从第 16 次起,JVM 通过字节码生成工具(如 ASM)动态生成 GeneratedMethodAccessor 类,该类将反射调用转换为虚拟调用(virtual dispatch),使 JIT 编译器可以对其应用常规优化(如内联)。因此反射调用的”预热”很重要——对于需要频繁反射调用的热点路径,前 15 次调用后性能会显著提升。
Q5: MethodHandle 和反射(Method.invoke)有什么区别?什么时候应该使用 MethodHandle?
MethodHandle 是 Java 7 引入的 JVM 级函数指针,与反射的主要区别:(1) MethodHandle 在创建时就完成了类型链接和访问检查(而非每次调用时检查),invokeExact 调用几乎零开销;(2) MethodHandle 支持方法句柄变换(MethodHandles.filterArguments、guardWithTest 等),可以在 MethodHandle 层面组合调用逻辑;(3) MethodHandle 可以被 JIT 内联(特别是常量 MethodHandle);(4) MethodHandle 的签名在创建时确定,调用时严格类型匹配(invokeExact)。使用建议:对于热点路径的频繁调用,优先使用 MethodHandle + LambdaMetafactory;对于通用场景(工具类、框架)使用反射更灵活。
参考源码路径:
- java.lang.reflect 包:
$JAVA_HOME/src/java.base/share/classes/java/lang/reflect/ - ART 反射实现:
art/runtime/reflection.cc - 隐藏 API 限制:
art/runtime/hidden_api.h - Retrofit 动态代理:
https://github.com/square/retrofit







