Java 反射机制 -- Java 语言反射的概述、核心类与高级应用

avatar
作者
筋斗云
阅读量:0

大家好,我是栗筝i,这篇文章是我的 “栗筝i 的 Java 技术栈” 专栏的第 010 篇文章,在 “栗筝i 的 Java 技术栈” 这个专栏中我会持续为大家更新 Java 技术相关全套技术栈内容。专栏的主要目标是已经有一定 Java 开发经验,并希望进一步完善自己对整个 Java 技术体系来充实自己的技术栈的同学。与此同时,本专栏的所有文章,也都会准备充足的代码示例和完善的知识点梳理,因此也十分适合零基础的小白和要准备工作面试的同学学习。当然,我也会在必要的时候进行相关技术深度的技术解读,相信即使是拥有多年 Java 开发经验的从业者和大佬们也会有所收获并找到乐趣。

Reflection(反射)是被视为动态语言的关键,反射机制允许程序在执行期借助于 ReflectionAPI 取得任何类的内部信息,并能直接操作任意对象的内 部属性及方法。


文章目录


1、Java 反射概述

1.1、反射的机制介绍

Java 反射(Reflection)是一种运行时机制,它反射允许程序在运行时探查和修改自身的行为和结构。通过反射,我们可以在运行时获取类的完整信息,包括类的成员变量、方法、构造函数等,并可以动态调用对象的方法、修改对象的字段值、创建实例等。这使得反射成为一种强大的工具,特别是在需要灵活和动态行为的场景中。

在这里插入图片描述

Java 反射的引入可以追溯到 Java 1.1版本。它是 Java 语言设计者为了增强 Java 的动态性和灵活性而引入的特性。反射机制在早期主要用于框架开发和一些高级编程技术。随着 Java 语言的不断发展,反射逐渐成为许多Java框架(如Spring、Hibernate)和库中不可或缺的一部分。

1.2、Java 的准动态特性

在编程语言中,语言可以大致分为动态语言和静态语言两类:

  • 动态语言:如 Python、Ruby 等,它们在运行时确定变量的类型,并允许在运行时改变类和对象的结构。这种语言的主要特点是高度的灵活性和动态性;
  • 静态语言:如 Java、C++ 等,它们在编译时确定变量的类型,并且类和对象的结构在运行时是固定的。这种语言的主要特点是类型安全和性能较高。

Java 作为一种静态语言,通过引入反射机制,获得了一些动态语言的特性。反射使得 Java 能够在运行时动态获取类的信息,并对其进行操作,使其在一定程度上具备了动态语言的灵活性。这种特性使 Java 成为一种准动态语言,既保持了静态语言的类型安全和性能优势,又具备了一定的动态性,使其在复杂的应用场景中更加灵活和强大。

通过反射,Java 能够实现:

  • 动态加载和调用类的方法
  • 动态修改对象的属性
  • 动态生成和操作对象

这种动态特性极大地增强了 Java 的灵活性,使其能够应对更多变的应用需求,并为诸如 Spring、Hibernate 等框架的动态配置和依赖注入提供了基础。

1.3、反射的实现原理

使用 Java 反射机制的前提条件是:必须先得到代表字节码的 Class 对象。Class 类用于表示 .class 文件(字节码)。所有的反射操作都是从 Class 对象开始的。

Java 反射的底层实现依赖于 JVM 的内部机制。在 JVM 加载类时,会将类的结构信息存储在内存中(通常是方法区或元空间)。这些信息包括类的名称、字段、方法、构造函数、修饰符等。Java 反射 API 通过访问这些内存中的结构信息,实现对类的探测和操作。

Java 反射机制的原理如下:

  1. 编译阶段:我们编写的源代码是 .java 文件,通过 javac 编译后成为 .class 文件,即字节码文件;

  2. 类加载阶段:

    • 程序执行时,Java 虚拟机会将字节码文件加载到内存中,具体来说是加载到方法区(Java 8 之前)或元空间(Java 8 及以后);
    • 加载的过程中,JVM 会解析 .class 文件,将类的结构信息(如类名、字段、方法、构造函数、修饰符等)存储在内存中;
  3. 运行时阶段:程序运行时,通过获取 Class 对象,我们可以访问类的结构信息;

  4. 反射操作:通过 Class 对象,我们可以动态获取类的信息,并进行各种操作,例如:获取并修改类的字段值、获取并调用类的方法、获取构造函数并创建实例。

1.4、反射动态性和应用

反射机制允许程序在运行时动态获取类的信息,并进行实例化和调用。这个特性在需要灵活和动态行为的场景中非常有用。例如:

  • 依赖注入和 IoC 容器:Spring 框架通过 XML 文件描述类的基本信息,使用反射机制动态装配对象;
  • 对象关系映射(ORM):如 Hibernate 框架使用反射将 Java 对象与数据库表进行映射,从而简化数据库操作;
  • 序列化和反序列化:Java 自带的序列化机制,通过反射实现对象的序列化和反序列化。

通过反射机制,Java 程序可以在运行时动态地访问和操作类的成员,极大地增强了程序的灵活性和动态性。


2、反射机制的核心类

2.1、反射机制的核心类和接口

Java 反射机制的实现依赖于 java.lang.reflect 包中的几个核心类和接口。这些类和接口使得 Java 程序能够在运行时检查和操作类的结构和行为。以下是 Java 反射机制中几个关键的核心类:

  1. Class 类:用于表示类或接口的运行时类型,提供了获取类的名称、修饰符、超类、实现的接口、成员变量、方法和构造函数等信息的方法,获取 Class 对象的方式有:通过类名、类字面值和对象实例;
  2. Field 类:表示类的成员变量(字段),提供了获取字段的名称、类型、修饰符等信息的方法,可以通过 Field 对象读取和修改字段的值;
  3. Method 类:表示类的方法,提供了获取方法的名称、返回类型、参数类型、修饰符等信息的方法,可以通过 Method 对象调用方法;
  4. Constructor 类:表示类的构造函数,提供了获取构造函数的参数类型、修饰符等信息的方法,可以通过 Constructor 对象创建类的实例;
  5. Array 类:提供了用于动态创建和操作数组的静态方法,可以创建数组实例、获取数组长度以及读取和修改数组元素;
  6. Modifier 类:用于解码类或成员的访问修饰符,提供了方法来判断类或成员是否具有特定的修饰符,如 publicprivateprotectedstaticfinal 等。

这些核心类为 Java 反射机制提供了基础,使得程序可以在运行时动态地探查和操作类的结构和行为,极大地增强了 Java 的灵活性和动态性。

2.2、Class 类

Class 类是反射的基础,它表示类或接口的运行时类型。Class 对象包含了类的名称、修饰符、超类、实现的接口、成员变量、方法和构造函数等信息。

2.2.1、理解 Class 类

要想理解反射,首先要理解 Java.lang.Class 类,因为 Java.lang.Class 类是反射实现的基础。

public final class Class<T> implements java.io.Serializable,                               GenericDeclaration,                               Type,                               AnnotatedElement { 

在程序运行期间,JVM 始终为所有的对象维护一个被称为运行时的类型标识,这个信息跟踪着每个对象所属的类的完整结构信息,包括包名、类名、实现的接口、拥有的方法和字段等。可以通过专门的 Java 可以访问这些信息,这个类就是 Class 类。我们可以把Class 类型理解为类的类型,一个 Class 对象,称为类型的类型对象,一个 Class 对象对应一个加载到 JVM 中的一个 .class 文件。

在通常情况下,一定是先有类再有对象。以下面这段代码为例,类的正常加载过程是这样的:

// 先有类 import java.util.Date;    public class Test {     public static void main(String[] args) {        // 后有对象         Date date = new Date();          System.out.println(date);     } } 

首先 Jvm 会将我们的代码编译成一个 .class 字节码文件,然后被类加载器(ClassLoader)加载进 Jvm 内存中,同时会创建一个 Date 类的 Java.lang.Class 对象存到堆中(注意这个不是 new 出来的对象,而是类的类型对象)。Jvm 在创建 Date 对象前,会先检查其类是否加载,寻找类对应的 Java.lang.Class 对象,若加载好,则为其分配内存,然后再进行初始化 new Date()

img

需要注意的是,每个类别只有一个 Java.lang.Class 对象,也就是说如果我们有第二条 new Date() 语句,Jvm 不会再生成一个 Date 的 Java.lang.Class 对象,因为已经存在一个了。

这也使得我们可以利用 == 运算符实现两个类对象比较的操作:

System.out.println(date.getClass() == Date.getClass()); // true 

OK,那么在加载完一个类后,堆内存的方法区就产生了一个 Class 对象,这个对象就包含了完整的类的结构信息,我们可以通过这个 Class 对象看到类的结构,就好比一面镜子。所以我们形象地称之为:反射。

说得再详细点,再解释一下。上文说过,在通常情况下,一定是先有对象再有对象,我们把这个通常情况称为 “正”。那么反射中的这个 “反” 我们就可以理解为根据对象找到对象所属的类(对象的出处)。

Date date = new Date(); System.out.println(date.getClass()); // "class java.util.Date" 

通过反射,也就是调用了 getClass() 方法后,我们就获得了 Date 类对应的 Java.lang.Class 对象,看到了 Date 类似的结构,输出了 Date 对象所属的类的完整名称,即找到了对象的出处。当然,获取 Java.lang.Class 对象的方式不止这一种。

2.2.2、获取 Class 对象的方式

在 Java 中,java.lang.Class 类的构造函数是私有的,这意味着我们不能像创建普通类的对象那样直接通过 new 关键字来创建 Class 类的对象。只有 JVM 可以创建 Class 类的对象。

我们可以通过以下四种方式来获取 Class 类的对象:

方式一:如果我们知道具体的类,可以直接使用 .class 来获取 Class 类的对象:

Class targetObjectClass = TargetObject.class; 

通过这种方式获取的 Class 对象不会进行初始化。

方式二:我们可以通过 Class.forName() 方法并传入全类名来获取 Class 类的对象:

Class targetObjectClass1 = Class.forName("com.xxx.TargetObject"); 

这个方法内部实际上调用的是 forName() 方法。forName() 方法的第二个参数表示类是否需要初始化,默认是需要初始化。一旦初始化,就会触发目标对象的静态块代码的执行,静态参数也会被再次初始化。

方式三:我们可以通过对象实例的 getClass() 方法来获取 Class 类的对象:

Date date = new Date(); Class dateClass = date.getClass(); 

方式四:我们还可以通过类加载器的 loadClass() 方法并传入类路径来获取 Class 类的对象:

Class targetObjectClass2 = ClassLoader.loadClass("com.xxx.TargetObject"); 

通过类加载器获取的 Class 对象不会进行初始化,这意味着不会进行包括初始化等一系列步骤,静态块和静态对象不会得到执行。这里可以和 forName() 方法做一个对比。

2.2.3、获取类的信息

获取类名:

String className = clazz.getName(); 

获取修饰符:

int modifiers = clazz.getModifiers(); boolean isPublic = Modifier.isPublic(modifiers); 

获取超类:

Class<?> superClass = clazz.getSuperclass(); 

获取实现的接口:

Class<?>[] interfaces = clazz.getInterfaces(); 

获取成员变量:

Field[] fields = clazz.getDeclaredFields(); 

获取方法:

Method[] methods = clazz.getDeclaredMethods(); 

获取构造函数:

Constructor<?>[] constructors = clazz.getDeclaredConstructors(); 
2.3、Field 类

Field 类表示类的成员变量(字段)。通过 Field 对象,可以获取字段的名称、类型、修饰符等信息,并可以读取和修改字段的值。

获取字段:

Field field = clazz.getDeclaredField("fieldName"); field.setAccessible(true); // 设置为可访问 

读取和修改字段值:

Object value = field.get(instance); field.set(instance, newValue); 
2.4、Method 类

Method 类表示类的方法。通过 Method 对象,可以获取方法的名称、返回类型、参数类型、修饰符等信息,并可以调用该方法。

获取方法:

Method method = clazz.getDeclaredMethod("methodName", parameterTypes); method.setAccessible(true); 

调用方法:

Object result = method.invoke(instance, args); 
2.5、Constructor 类

Constructor 类表示类的构造函数。通过 Constructor 对象,可以获取构造函数的参数类型、修饰符等信息,并可以创建实例。

获取构造函数:

Constructor<?> constructor = clazz.getDeclaredConstructor(parameterTypes); constructor.setAccessible(true); 

创建实例:

Object instance = constructor.newInstance(args); 
2.6、Array 类

Array 类提供了用于动态创建和操作数组的静态方法。通过 Array 类,可以创建数组实例、获取数组长度以及读取和修改数组元素。

Array 类虽然不是反射操作中最常用的类,但它在反射机制中提供了一些有用的方法,使得我们可以通过反射来创建和操作数组。这使得我们可以在运行时动态地处理数组类型,而不需要在编译时确定数组的类型和大小。

2.6.1、动态创建数组

通过 Array 类的静态方法,我们可以动态地创建数组实例,而不需要在编译时确定数组的类型和大小。

创建数组实例:

int[] intArray = (int[]) Array.newInstance(int.class, 10); // 创建一个长度为 10 的 int 数组 String[] strArray = (String[]) Array.newInstance(String.class, 5); // 创建一个长度为 5 的 String 数组 
2.6.2、操作数组元素

通过 Array 类,我们可以在运行时动态地获取和设置数组元素的值。这在处理类型未知或动态类型的数组时非常有用。

获取数组长度:

int length = Array.getLength(intArray); 

读取数组元素:

int value = (int) Array.get(intArray, 0); // 获取 intArray 的第一个元素 String str = (String) Array.get(strArray, 1); // 获取 strArray 的第二个元素 

修改数组元素:

Array.set(intArray, 0, 100); // 设置 intArray 的第一个元素为 100 Array.set(strArray, 1, "Hello"); // 设置 strArray 的第二个元素为 "Hello" 
2.7、AnnotatedElement 接口

AnnotatedElement 接口是所有可以包含注解的元素的超接口,包括 ClassMethodFieldConstructor 等。它提供了获取注解的核心方法。

获取注解的方法:

  • isAnnotationPresent(Class<? extends Annotation> annotationClass):检查是否存在指定类型的注解;
  • getAnnotation(Class<T> annotationClass):获取指定类型的注解;
  • getAnnotations():获取所有注解;
  • getDeclaredAnnotations():获取所有声明的注解(包括私有的)。

3、反射的高级应用

Java 反射不仅可以用于基本的类探测和操作,还能在一些高级编程技术中发挥重要作用。以下是反射的几个高级应用,包括动态代理、框架中的反射应用以及自定义注解处理。

3.1、动态代理

动态代理是一种设计模式,通过它可以在运行时创建代理类,而不需要在编译时定义具体的代理类。Java 提供了 java.lang.reflect.Proxy 类和 java.lang.reflect.InvocationHandler 接口来实现动态代理。

动态代理的使用场景:

  • 日志记录:在方法调用前后自动记录日志;
  • 事务管理:在方法调用前开启事务,方法调用后提交或回滚事务;
  • 访问控制:在方法调用前检查权限。

动态代理示例:

import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy;  // 定义一个接口 Service interface Service {     void perform(); }  // 实现 Service 接口的具体类 RealService class RealService implements Service {     public void perform() {         System.out.println("Performing service...");     } }  // 实现 InvocationHandler 接口的代理处理器类 class ServiceInvocationHandler implements InvocationHandler {     private final Object target;      // 构造方法,接受一个目标对象     public ServiceInvocationHandler(Object target) {         this.target = target;     }      @Override     public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {         System.out.println("Before method call");         // 调用目标对象的方法         Object result = method.invoke(target, args);         System.out.println("After method call");         return result;     } }  // 示例类,用于展示动态代理的使用 public class DynamicProxyExample {     public static void main(String[] args) {         // 创建真实的服务对象         RealService realService = new RealService();         // 创建代理实例         Service proxyInstance = (Service) Proxy.newProxyInstance(             realService.getClass().getClassLoader(), // 类加载器             realService.getClass().getInterfaces(), // 目标对象实现的接口             new ServiceInvocationHandler(realService) // 代理处理器         );          // 调用代理对象的方法         proxyInstance.perform();     } } 
3.2、框架中的反射应用

许多流行的 Java 框架都广泛使用反射来实现其功能。这些框架利用反射机制在运行时动态创建和管理对象,从而提供高度的灵活性和扩展性。

3.2.1、Spring 框架

Spring 框架:

  • 依赖注入(Dependency Injection):Spring 通过反射扫描类路径下的组件,读取注解或 XML 配置,动态创建和注入依赖对象。
  • 面向切面编程(AOP):Spring 使用动态代理或字节码操作在运行时生成代理类,织入切面逻辑,如方法拦截、事务管理等。

Spring 依赖注入示例:

import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.stereotype.Service;  // 使用 @Service 注解标识 MyService 类,表示它是一个服务组件 @Service public class MyService {     public void serve() {         System.out.println("Service is serving...");     } }  // 使用 @Controller 注解标识 MyController 类,表示它是一个控制器组件 @Controller public class MyController {          // 使用 @Autowired 注解标识 myService 字段,表示它需要自动注入 MyService 实例     @Autowired     private MyService myService;      public void handleRequest() {         myService.serve();     } } 
3.2.2、Hibernate 框架

Hibernate 框架:对象关系映射(ORM):Hibernate 通过反射读取实体类的注解或 XML 配置,将 Java 对象与数据库表进行映射,自动生成 SQL 语句来操作数据库。

Hibernate 映射示例:

@Entity @Table(name = "users") public class User {     @Id     @GeneratedValue(strategy = GenerationType.IDENTITY)     private Long id;      @Column(name = "username")     private String username;      @Column(name = "password")     private String password;      // Getters and setters } 
3.3、自定义注解处理

自定义注解是 Java 5 引入的一项强大功能,允许开发者定义自己的注解,并通过反射机制在运行时处理这些注解。自定义注解可以用于各种用途,如配置、验证、日志记录等。

定义自定义注解:

@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface LogExecutionTime { } 

处理自定义注解:

import java.lang.reflect.Method;  // 注解处理器类 public class AnnotationProcessor {          // 处理注解的方法     public static void processAnnotations(Object obj) {         // 获取类的 Class 对象         Class<?> clazz = obj.getClass();                  // 遍历类中的所有方法         for (Method method : clazz.getDeclaredMethods()) {             // 检查方法是否包含 LogExecutionTime 注解             if (method.isAnnotationPresent(LogExecutionTime.class)) {                 System.out.println("Method " + method.getName() + " is annotated with @LogExecutionTime");             }         }     }      public static void main(String[] args) {         // 定义一个内部类 TestClass,包含带有 LogExecutionTime 注解的方法         class TestClass {             @LogExecutionTime             public void testMethod() {                 // 方法实现                 System.out.println("Executing testMethod");             }         }          // 创建 TestClass 实例         TestClass testClass = new TestClass();                  // 处理 TestClass 实例中的注解         processAnnotations(testClass);     } } 

通过反射机制,Java 提供了强大的动态操作能力,使得框架和工具可以在运行时灵活地处理各种应用场景。这些高级应用不仅增强了 Java 程序的灵活性和动态性,还使得开发工作更加简洁和高效。

3.4、其他高阶应用

除了前面介绍的动态代理、框架中的反射应用以及自定义注解处理,反射在动态加载和插件机制、单元测试和模拟对象中也有广泛应用。以下是对这两方面的详细介绍:

3.4.1、动态加载和插件机制

反射可以用于动态加载类和实现插件机制,使得应用程序可以在运行时加载和使用新功能,而不需要在编译时知道这些类或插件的具体实现。

动态加载类:通过反射,可以在运行时动态加载类,而不需要在编译时确定具体的类。这对于实现可扩展和可插拔的系统非常有用。

public class DynamicClassLoadingExample {     public static void main(String[] args) {         try {             // 动态加载类             Class<?> clazz = Class.forName("com.example.MyPlugin");             // 创建实例             Object plugin = clazz.getDeclaredConstructor().newInstance();             // 调用方法             Method method = clazz.getDeclaredMethod("execute");             method.invoke(plugin);         } catch (Exception e) {             e.printStackTrace();         }     } } 

插件机制:动态加载类和接口实现可以实现插件机制,使得应用程序可以在运行时加载和执行插件。

// 插件接口 public interface Plugin {     void execute(); }  // 插件实现 public class MyPlugin implements Plugin {     public void execute() {         System.out.println("Plugin executed.");     } }  public class PluginLoader {     public static void main(String[] args) {         try {             // 动态加载插件类             Class<?> clazz = Class.forName("com.example.MyPlugin");             // 创建插件实例             Plugin plugin = (Plugin) clazz.getDeclaredConstructor().newInstance();             // 执行插件方法             plugin.execute();         } catch (Exception e) {             e.printStackTrace();         }     } } 
3.4.2、单元测试和模拟对象

反射在单元测试和创建模拟对象(mock objects)方面也有重要应用。它使得测试框架可以在运行时创建对象、调用方法和设置字段值,而不需要直接依赖于对象的具体实现。

单元测试:在单元测试中,可以通过反射调用私有方法或访问私有字段,避免修改源代码的访问级别。

public class PrivateMethodTest {     private String secretMethod() {         return "secret";     }      public static void main(String[] args) {         try {             PrivateMethodTest test = new PrivateMethodTest();             // 通过反射访问私有方法             Method method = PrivateMethodTest.class.getDeclaredMethod("secretMethod");             method.setAccessible(true);             String result = (String) method.invoke(test);             System.out.println("Result: " + result); // 输出 "Result: secret"         } catch (Exception e) {             e.printStackTrace();         }     } } 

模拟对象(Mock Objects):在单元测试中,可以使用反射创建模拟对象,替代实际的依赖对象,从而隔离被测试的代码。

import org.junit.jupiter.api.Test; import static org.mockito.Mockito.*;  public class MockExample {     public interface Service {         String serve();     }      public class Client {         private Service service;          public Client(Service service) {             this.service = service;         }          public String requestService() {             return service.serve();         }     }      @Test     public void testService() throws Exception {         // 创建模拟对象         Service mockService = mock(Service.class);         when(mockService.serve()).thenReturn("mocked service");          // 通过反射设置私有字段         Client client = new Client(mockService);         Field field = Client.class.getDeclaredField("service");         field.setAccessible(true);         field.set(client, mockService);          // 测试方法         String result = client.requestService();         System.out.println("Result: " + result); // 输出 "Result: mocked service"     } } 

通过这些高级应用,反射机制使得 Java 程序具备了高度的灵活性和动态性,能够适应各种复杂的应用场景,包括动态加载、插件机制、单元测试和模拟对象。

广告一刻

为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!