大家好,我是栗筝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 反射机制的原理如下:
编译阶段:我们编写的源代码是
.java
文件,通过javac
编译后成为.class
文件,即字节码文件;类加载阶段:
- 程序执行时,Java 虚拟机会将字节码文件加载到内存中,具体来说是加载到方法区(Java 8 之前)或元空间(Java 8 及以后);
- 加载的过程中,JVM 会解析
.class
文件,将类的结构信息(如类名、字段、方法、构造函数、修饰符等)存储在内存中;
运行时阶段:程序运行时,通过获取
Class
对象,我们可以访问类的结构信息;反射操作:通过
Class
对象,我们可以动态获取类的信息,并进行各种操作,例如:获取并修改类的字段值、获取并调用类的方法、获取构造函数并创建实例。
1.4、反射动态性和应用
反射机制允许程序在运行时动态获取类的信息,并进行实例化和调用。这个特性在需要灵活和动态行为的场景中非常有用。例如:
- 依赖注入和 IoC 容器:Spring 框架通过 XML 文件描述类的基本信息,使用反射机制动态装配对象;
- 对象关系映射(ORM):如 Hibernate 框架使用反射将 Java 对象与数据库表进行映射,从而简化数据库操作;
- 序列化和反序列化:Java 自带的序列化机制,通过反射实现对象的序列化和反序列化。
通过反射机制,Java 程序可以在运行时动态地访问和操作类的成员,极大地增强了程序的灵活性和动态性。
2、反射机制的核心类
2.1、反射机制的核心类和接口
Java 反射机制的实现依赖于 java.lang.reflect
包中的几个核心类和接口。这些类和接口使得 Java 程序能够在运行时检查和操作类的结构和行为。以下是 Java 反射机制中几个关键的核心类:
- Class 类:用于表示类或接口的运行时类型,提供了获取类的名称、修饰符、超类、实现的接口、成员变量、方法和构造函数等信息的方法,获取
Class
对象的方式有:通过类名、类字面值和对象实例; - Field 类:表示类的成员变量(字段),提供了获取字段的名称、类型、修饰符等信息的方法,可以通过
Field
对象读取和修改字段的值; - Method 类:表示类的方法,提供了获取方法的名称、返回类型、参数类型、修饰符等信息的方法,可以通过
Method
对象调用方法; - Constructor 类:表示类的构造函数,提供了获取构造函数的参数类型、修饰符等信息的方法,可以通过
Constructor
对象创建类的实例; - Array 类:提供了用于动态创建和操作数组的静态方法,可以创建数组实例、获取数组长度以及读取和修改数组元素;
- Modifier 类:用于解码类或成员的访问修饰符,提供了方法来判断类或成员是否具有特定的修饰符,如
public
、private
、protected
、static
、final
等。
这些核心类为 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()
。
需要注意的是,每个类别只有一个 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
接口是所有可以包含注解的元素的超接口,包括 Class
、Method
、Field
、Constructor
等。它提供了获取注解的核心方法。
获取注解的方法:
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 程序具备了高度的灵活性和动态性,能够适应各种复杂的应用场景,包括动态加载、插件机制、单元测试和模拟对象。