Java 反射原理及应用

第一部分:反射的基本概念

1. 什么是反射?

反射是指在程序运行状态 中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性。这种动态获取信息以及动态调用对象方法的功能称为 Java 语言的反射机制。

2. 为什么需要反射?

在正常情况下,我们使用某个类时,都知道这个类是什么,有什么方法,然后直接通过 new 关键字创建对象并调用方法。这是一种"正向"操作。

而反射则相反,它允许我们在编译期不确定具体类型 的情况下,在运行期动态地加载类、创建对象、调用方法。这极大地提高了程序的灵活性和扩展性

核心价值:反射将类的"动态加载"和"动态绑定"能力交给了程序员,使得框架(如 Spring)、工具(如 JUnit)和 IDE 能够实现。


第二部分:反射的核心 API

反射的核心 API 位于 java.lang.reflect 包中,主要涉及以下几个类:

  • Class 类:代表一个类或接口。反射的根源。

  • Field 类:代表类的成员变量。

  • Method 类:代表类的方法。

  • Constructor 类:代表类的构造方法。

获取 Class 对象的三种方式:

这是反射的起点。

  1. Class.forName("全限定类名"):通过类的全路径名获取。最常用,体现了动态加载。

  2. 对象.getClass():通过对象实例获取。

  3. 类名.class:通过类字面常量获取。

java 复制代码
// 1. Class.forName()
Class<?> clazz1 = Class.forName("java.lang.String");

// 2. .getClass()
String str = "Hello";
Class<?> clazz2 = str.getClass();

// 3. .class
Class<?> clazz3 = String.class;

System.out.println(clazz1 == clazz2); // true
System.out.println(clazz2 == clazz3); // true

第三部分:反射的底层原理

这是理解反射性能开销和实现机制的关键。

1. Class 对象 ------ 反射的基石

JVM 在加载一个类时(比如 java.lang.String),会在堆内存中为其创建一个唯一的 java.lang.Class 对象。这个 Class 对象就像是该类的"蓝图"或"镜像",它包含了该类的所有结构信息:

  • 类名、包名、父类、实现的接口

  • 字段信息(Field[]

  • 方法信息(Method[]

  • 构造器信息(Constructor[]

  • 访问修饰符等

无论你通过哪种方式获取 Class 对象,最终指向的都是 JVM 为这个类创建的同一个 Class 对象。

2. 方法的反射调用与 MethodAccessor

当我们通过 Method.invoke(obj, args...) 调用方法时,底层发生了什么?

在 JDK 1.8 及之前,invoke 的调用路径大致如下:
Method.invoke -> DelegatingMethodAccessorImpl.invoke -> NativeMethodAccessorImpl.invoke -> JNI(本地方法调用) -> 调用底层 C++ 代码。

  • 初始状态 :第一次调用时,会使用一个本地实现(NativeMethodAccessorImpl),它通过 JNI 调用到 JVM 内部的 native 代码来执行方法。这个调用路径很长,涉及 Java 到 C++ 的切换,所以性能开销很大

  • 动态生成字节码 :为了优化,Sun JDK 采用了一种技术。当一个方法被反射调用的次数超过一个阈值(默认15次)时,会动态生成一个 MethodAccessor 的实现类,其 invoke 方法的字节码是直接用字节码生成技术(如 sun.reflect.ClassFileGenerator)在内存中创建的

  • 这个生成的类,我们姑且称之为 GeneratedMethodAccessor1,它内部会直接调用目标方法,就像我们手写的 obj.targetMethod(args) 一样。这种方式避免了 JNI 的开销,因此后续的调用会快很多。这个过程被称为 Inflation(膨胀)

为什么要有这个阈值?

因为生成字节码本身也是有成本的。对于只调用一两次的方法,生成字节码得不偿失,不如直接用本地方法。对于频繁调用的方法,生成字节码则能带来长期的性能收益。

在 JDK 1.9 及之后(模块化):

反射的实现被移到了 java.lang.invoke 包下,并且底层更多地依赖于 MethodHandle(方法句柄)。MethodHandle 在性能上通常优于传统的反射,并且提供了更精确的类型匹配。但基本原理类似,都是为了找到一种高效的方式来动态调用方法。

3. 反射的性能开销来源
  1. 方法调用开销:早期的 JNI 调用或动态字节码的生成和验证。

  2. 访问权限检查 :每次调用 Field.get/setMethod.invoke 时,JVM 都需要检查访问权限(如 private, protected)。即使我们调用了 setAccessible(true) 来取消检查,这个调用本身也有开销。

  3. 自动装箱/拆箱 :反射方法的参数是 Object[],所以基本类型需要频繁地进行装箱和拆箱。

  4. 方法签名解析:需要根据传入的参数类型来匹配最合适的方法。

优化建议

  • 在性能敏感的循环中,避免使用反射。

  • 缓存 Class, Method, Field 等对象。不要每次使用都重新查找。

  • 对需要频繁访问的私有字段/方法,调用 setAccessible(true) 来禁用安全检查。


第四部分:反射的应用场景

反射是许多 Java 框架和库的基石。

  1. Spring 框架的 IoC 容器

    • Spring 在启动时,会扫描指定路径下的类,通过反射获取类的信息(如 @Component, @Autowired 等注解)。

    • 当需要创建 Bean 时,Spring 通过反射调用类的构造器(Constructor.newInstance())来实例化对象。

    • 对于依赖注入,Spring 通过反射(Field.set()Method.invoke())将依赖的 Bean 设置到目标字段或 setter 方法上。

  2. JUnit 测试框架

    • JUnit 通过反射来查找测试类中所有带有 @Test 注解的方法。

    • 然后为每个测试方法创建一个新的测试类实例,并通过反射调用这些测试方法。

  3. MyBatis 等 ORM 框架

    • 将从数据库查询出的 ResultSet 数据,通过反射(Field.set())映射到实体类(POJO)的对应字段上。
  4. 动态代理

    • JDK 动态代理(java.lang.reflect.Proxy)的核心就是利用反射,在运行时生成一个实现了指定接口的代理类,并将所有方法调用都转发到 InvocationHandler.invoke() 方法中。
  5. IDE 的智能提示

    • IDE 通过反射来分析我们编写的代码,获取类的结构,从而提供代码补全、方法提示、类型检查等功能。

第五部分:代码示例

java 复制代码
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class ReflectionDemo {

    public static void main(String[] args) throws Exception {
        // 1. 获取Class对象
        Class<?> clazz = Class.forName("java.util.ArrayList");

        // 2. 创建对象 (调用无参构造器)
        Object list = clazz.getDeclaredConstructor().newInstance();

        // 3. 获取方法并调用
        Method addMethod = clazz.getMethod("add", Object.class);
        addMethod.invoke(list, "Hello, Reflection!");
        addMethod.invoke(list, 123);

        // 4. 获取size方法并调用
        Method sizeMethod = clazz.getMethod("size");
        int size = (int) sizeMethod.invoke(list);
        System.out.println("List size: " + size); // 输出: List size: 2

        // 5. 访问内部数组字段 (注意:这是一个示例,实际ArrayList的elementData是private的)
        // 通过setAccessible(true)绕过访问权限检查
        Field elementDataField = clazz.getDeclaredField("elementData");
        elementDataField.setAccessible(true); // 关键!取消访问检查
        Object[] elementData = (Object[]) elementDataField.get(list);
        System.out.println("Internal array length: " + elementData.length);

        // 6. 打印List内容 (实际会调用toString,这里只是演示)
        System.out.println("List content: " + list);
    }
}

总结

方面 要点
核心思想 在运行时动态分析和操作类、对象、方法和属性。
底层原理 基于 JVM 的 Class 对象,通过本地方法或动态生成的字节码来实现动态调用。
性能开销 主要来自方法调用、权限检查、装箱拆箱。可通过缓存和 setAccessible 优化。
主要应用 框架设计(Spring IoC, MyBatis)、测试(JUnit)、动态代理、IDE 等。
优点 灵活性高、扩展性强,是构建复杂、可扩展架构的利器。
缺点 性能稍差、安全性问题(可绕过封装)、代码可读性降低

理解反射的底层原理,能帮助你在"魔法般"的框架使用中,洞悉其本质,并能在合适的场景下正确地使用和优化它。

相关推荐
莎士比亚的文学花园1 小时前
数据库——SQLite使用教程
数据库
myloveasuka2 小时前
权限修饰符&代码块
java
柒.梧.2 小时前
Java集合核心知识点深度解析:数组与集合区别、ArrayList原理及线程安全问题
java·开发语言·python
0 0 02 小时前
洛谷P4427 [BJOI2018] 求和 【考点】:树上前缀和
开发语言·c++·算法·前缀和
web3.08889992 小时前
使用PHP采集数据的完整技术文章,涵盖多种场景和最佳实践
开发语言·php
柒.梧.2 小时前
Java基础高频面试题(含详细解析+易错点,面试必看)
java·开发语言·面试
yuweiade2 小时前
Redis服务安装自启动(Windows版)
数据库·windows·redis
佩奇大王2 小时前
P593 既约分数
java·开发语言·算法
小同志002 小时前
软件测试周期 与 BUG
java·软件测试·bug·软件测试周期·bug等级