Java学习十七—反射机制:解锁代码的无限可能
一、关于反射
1.1 简介
Java 反射(Reflection)是Java 的特征之一,它允许程序在运行时动态地访问和操作类的信息,包括类的属性、方法和构造函数。
反射机制能够使程序具备更大的灵活性和扩展性
Oracle 官方对反射的解释:
Reflection enables Java code to discover information about the fields, methods and constructors of loaded classes, and to use reflected fields, methods, and constructors to operate on their underlying counterparts, within security restrictions.
The API accommodates applications that need access to either the public members of a target object (based on its runtime class) or the members declared by a given class. It also allows programs to suppress default reflective access control.
1.2 发展
Java 反射机制的发展历程可以分为几个重要的阶段,随着 Java 语言和平台的演进,反射的功能和使用场景也不断丰富。
1. Java 1.0 版本(1996 年)
- 初步引入:Java 在最初的版本中就引入了反射机制的基础概念,但功能较为有限。主要是为了支持动态加载类和基础的类信息查询。
2. Java 1.1 版本(1997 年)
- 增强功能:Java 1.1 对反射机制进行了增强,增加了对接口的支持,使得开发者可以更灵活地操作对象。
- 引入
java.lang.reflect
包:这个包提供了获取类的信息、调用方法、访问属性的功能。
3. Java 2(JDK 1.2,1998 年)
- 引入集合框架:反射与集合框架的结合使用得到了广泛关注,开发者能够通过反射创建和操作集合中的对象。
- 安全性增强:反射机制也开始关注安全性,引入了安全管理器,限制某些反射操作。
4. Java 5(JDK 1.5,2004 年)
- 泛型支持:Java 5 引入了泛型,反射也随之支持泛型类型的查询和操作,提升了类型安全性。
- 注解机制:引入了注解(Annotations),反射机制开始被广泛应用于框架中,通过反射读取和处理注解信息。
5. Java 6(2006 年)及后续版本
- 性能优化:随着反射在大型框架(如 Spring、Hibernate)中的广泛应用,Java 的开发团队逐步对反射的性能进行了优化,尽量减少反射操作的开销。
- 动态代理:Java 6 中的
java.lang.reflect.Proxy
类使得动态代理的实现成为可能,进一步增强了反射的应用场景。
6. Java 8(2014 年)
- Lambda 表达式:与反射结合使用,提升了函数式编程的灵活性。
- 增强的类型推断:使得反射在处理复杂数据类型时变得更加高效和安全。
7. Java 9 及后续版本
- 模块化系统:Java 9 引入了模块化(Project Jigsaw),反射的使用在模块间的访问控制上得到了新的关注。
- API 和性能持续改进:持续优化反射的 API 和性能,以适应现代应用程序的需求。
1.3 特点
优点:
- 灵活性:能够在运行时决定使用哪个类或方法。
- 动态性:支持在程序运行过程中动态生成类和对象。
缺点:
- 性能开销:反射操作相对较慢,因为涉及动态解析。
- 安全性问题:使用反射可能会破坏封装性,访问私有成员。
- 代码可读性:反射使代码难以理解和维护。
1.4 应用场景
很多人都认为反射在实际的 Java 开发应用中并不广泛,其实不然。当我们在使用 IDE(如 Eclipse,IDEA)时,当我们输入一个对象或类并想调用它的属性或方法时,一按点号,编译器就会自动列出它的属性或方法,这里就会用到反射。
反射最重要的用途就是开发各种通用框架。 很多框架(比如 Spring)都是配置化的(比如通过 XML 文件配置 Bean),为了保证框架的通用性,它们可能需要根据配置文件加载不同的对象或类,调用不同的方法,这个时候就必须用到反射,运行时动态加载需要加载的对象。
举一个例子,在运用 Struts 2 框架的开发中我们一般会在 struts.xml
里去配置 Action
,比如:
<action name="login" class="org.ScZyhSoft.test.action.SimpleLoginAction" method="execute"> <result>/shop/shop-index.jsp</result> <result name="error">login.jsp</result> </action>
配置文件与 Action
建立了一种映射关系,当 View 层发出请求时,请求会被 StrutsPrepareAndExecuteFilter
拦截,然后 StrutsPrepareAndExecuteFilter
会去动态地创建 Action 实例。比如我们请求 login.action
,那么 StrutsPrepareAndExecuteFilter
就会去解析struts.xml文件,检索action中name为login的Action,并根据class属性创建SimpleLoginAction实例,并用invoke方法来调用execute方法,这个过程离不开反射。
对与框架开发人员来说,反射虽小但作用非常大,它是各种容器实现的核心。而对于一般的开发者来说,不深入框架开发则用反射用的就会少一点,不过了解一下框架的底层机制有助于丰富自己的编程思想,也是很有益的。
以下是一些常见的应用场景:
1. 框架开发
- 依赖注入(Dependency Injection, DI) :框架如Spring使用反射来创建和管理Bean对象,实现依赖注入。通过反射,框架可以在运行时动态地将依赖对象注入到目标对象中。
- 面向切面编程(Aspect-Oriented Programming, AOP) :框架如Spring AOP使用反射来织入切面,实现横切关注点的分离。
- 对象关系映射(Object-Relational Mapping, ORM) :框架如Hibernate使用反射来映射数据库表和Java对象,实现持久化操作。
2. 单元测试
- 单元测试框架:框架如JUnit使用反射来发现和执行测试方法。通过反射,测试框架可以自动扫描类中的测试方法并执行它们。
- Mock对象:在单元测试中,经常需要模拟(mock)一些对象的行为。框架如Mockito使用反射来创建和管理这些Mock对象。
3. 动态代理
- Java动态代理:通过反射生成实现了特定接口的代理对象,用于实现AOP、远程调用(RPC)等功能。
- CGLIB动态代理:CGLIB是一个强大的高性能代码生成库,它可以在运行时生成一个类的子类对象,并使用反射来实现方法拦截。
4. 配置文件解析
- XML/JSON/YAML配置文件:许多框架和库使用反射来解析配置文件,并根据配置文件中的信息动态创建和配置对象。例如,Spring可以通过XML或注解配置文件来管理Bean。
5. 序列化和反序列化
- JSON库:库如Gson和Jackson使用反射来将Java对象转换为JSON字符串,或将JSON字符串转换为Java对象。
- XML库:库如JAXB使用反射来将Java对象转换为XML文档,或将XML文档转换为Java对象。
6. 插件系统
- 动态加载插件:通过反射,可以在运行时动态加载和卸载插件。这种机制常用于开发可扩展的应用程序,如IDE、游戏引擎等。
7. 数据绑定
- 数据绑定框架:框架如JavaFX和Vaadin使用反射来将用户界面组件的数据绑定到模型对象上,实现双向数据绑定。
8. 脚本语言集成
- 嵌入脚本语言:Java可以通过反射来调用嵌入的脚本语言(如JavaScript、Python)中的函数和方法,实现混合编程。
9. 安全性和权限管理
- 安全管理:通过反射可以动态地检查和设置对象的访问权限,实现细粒度的安全控制。
10. 日志记录
- 日志框架:框架如Log4j和SLF4J使用反射来获取类的信息,以便在日志记录中包含详细的上下文信息。
1.5 主要API
在Java中,反射主要涉及以下几个重要的类和接口:
-
Class类:
-
是进行反射的核心类。每个类在Java中都有一个与之对应的Class对象。
-
常用方法:
forName(String className)
:通过类的完全限定名获取Class对象。getDeclaredMethods()
:获取所有声明的方法,包括私有、保护和公共方法。getDeclaredFields()
:获取所有声明的字段。getDeclaredConstructors()
:获取所有声明的构造函数。newInstance()
:创建一个类的新实例。
-
-
Method类:
-
表示一个类的方法。
-
常用方法:
invoke(Object obj, Object... args)
:调用此Method对象所表示的方法。getName()
:获取方法的名称。getParameterTypes()
:获取方法的参数类型。getReturnType()
:获取方法的返回类型。
-
-
Field类:
-
表示一个类的字段(属性)。
-
常用方法:
get(Object obj)
:获取指定对象的字段值。set(Object obj, Object value)
:设置指定对象的字段值。getType()
:获取字段的类型。
-
-
Constructor类:
-
表示类的构造函数。
-
常用方法:
newInstance(Object... initargs)
:创建一个新对象的实例。getParameterTypes()
:获取构造函数的参数类型。
-
-
AccessibleObject类:
-
Method、Field和Constructor类都继承自AccessibleObject,提供了控制访问权限的能力。
-
常用方法:
setAccessible(boolean flag)
:设置对象是否可以进行访问(即使是私有)。
-
二、反射工作原理
调用反射的总体流程如下:
1、当编写完一个Java项目之后,每个java文件都会被编译成一个.class文件。
2、这些class文件在程序运行时会被ClassLoader加载到JVM中,当一个类被加载以后,JVM就会在内存中自动产生一个Class对象。
3、通过Class对象获取Field/Method/Construcor
我们一般平时是通过new的形式创建对象实际上就是通过这些Class来创建的,只不过这个class文件是编译的时候就生成的,程序相当于写死了给jvm去跑。
反射是什么呢?当我们的程序在运行时,需要动态的加载一些类这些类可能之前用不到所以不用加载到jvm,而是在运行时根据需要才加载。
原来使用new的时候,需要明确的指定类名,这个时候属于硬编码实现,而在使用反射的时候,可以只传入类名参数,就可以生成对象,降低了耦合性,使得程序更具灵活性。
二、反射的基本运用
反射可以用于判断任意对象所属的类,获得 Class 对象,构造任意一个对象以及调用一个对象。
这里介绍基本反射功能的使用和实现(反射相关的类一般都在 java.lang.relfect 包里)。
2.1 获取Class 对象
在Java中,获取一个类的Class
对象是使用反射的第一步,Class
对象用于操作类的信息。
方式
有以下集中方式可以获取Class类的实例:
-
若已知具体的类,可以通过类的class属性获取,该方式最为安全可靠,且程序性能最高。
//类的class属性 Class classOne = User.class;
-
已知某个类的实例,通过调用该实例的getClass方法获取Class对象。
//已有类对象的getClass方法 Class collatz = user.getClass();
-
已知一个类的全类名,且该类在类路径下,可以通过静态方法forName()获取。
Class c = Class.forName("com.dcone.zhuzqc.demo.User");
-
内置基本数据类型可以直接使用类名.Type获取。
//内置对象才有的TYPE属性,较大的局限性 Class<Integer> type = Integer.TYPE;
-
利用ClassLoader(类加载器)获取。
使用类加载器可以加载类并获取
Class
对象,特别是在自定义类加载的场景中:
ClassLoader classLoader = MyClass.class.getClassLoader(); Class<?> clazz = classLoader.loadClass("com.example.MyClass");
示例
public class ReflectionDemo { public static void main(String[] args) throws ClassNotFoundException { //获取 Class 对象 //方法1:使用 Class 类的 forName 静态方法(通过类的全名(包括包名)来获取) Class<?> baseAreaInfoDTOClass = Class.forName("com.gree.gpurchase.finance.infrastructure.dto.BaseAreaInfoDTO"); System.out.println(baseAreaInfoDTOClass); //方法2:使用类名.class Class<BaseAreaInfoDTO> dtoClass = BaseAreaInfoDTO.class; System.out.println(dtoClass); //方法3:调用某个对象的 getClass() 方法 BaseAreaInfoDTO baseAreaInfoDTO = new BaseAreaInfoDTO(); Class<? extends BaseAreaInfoDTO> infoDTOClass = baseAreaInfoDTO.getClass(); System.out.println(infoDTOClass); //方法4:使用 ClassLoader ClassLoader classLoader = BaseAreaInfoDTO.class.getClassLoader(); Class<?> bClass = classLoader.loadClass("com.gree.gpurchase.finance.infrastructure.dto.BaseAreaInfoDTO"); System.out.println(bClass); //示例:获取基本类型的 Class 对象 Class<?> intClass = int.class; Class<?> voidClass = void.class; System.out.println("int class对象:" + intClass); System.out.println("void class对象:" + voidClass); //示例:获取数组的 Class 对象 Class<BaseAreaInfoDTO[]> dClass = BaseAreaInfoDTO[].class; System.out.println("数组的class对象:" + dClass); int[] intArray = {5, 7}; Class<?> intArrayClass = intArray.getClass(); System.out.println(intArrayClass); String[] strArray = {"a", "b"}; Class<?> strArrayClass = strArray.getClass(); System.out.println(strArrayClass); } }
2.2 获取类的字段
使用反射获取类的成员变量(字段)的方法主要依赖于Class
类和Field
类。下面是详细的步骤,展示如何利用反射获取类的成员变量。
步骤
-
获取Class对象:
- 使用
Class.forName()
或者MyClass.class
方式获取目标类的Class
对象。
- 使用
-
获取字段:
- 可以使用
getDeclaredFields()
方法获取所有声明的字段,包括私有、保护和公共字段。 - 如果只想获取公共字段,可以使用
getFields()
方法。
- 可以使用
-
访问字段:
- 通过
Field
对象,可以获取字段的类型、名称和访问字段的值或者设置字段的值。
- 通过
示例
以下示例中定义了一个简单的类Person
,然后通过反射获取其成员变量。
import java.lang.reflect.Field; class Person { private String name; public int age; public Person(String name, int age) { this.name = name; this.age = age; } } public class ReflectionExample { public static void main(String[] args) { try { // 获取Person类的Class对象 Class<?> personClass = Person.class; // 获取所有声明的字段 Field[] fields = personClass.getDeclaredFields(); // 遍历字段并打印信息 for (Field field : fields) { // 获取字段名称 String fieldName = field.getName(); // 获取字段类型 Class<?> fieldType = field.getType(); System.out.println("Field Name: " + fieldName + ", Field Type: " + fieldType.getName()); // 如果字段是私有的,需要设置可访问性 if (!field.isAccessible()) { field.setAccessible(true); } // 创建实例以获取字段的值 Person person = new Person("John Doe", 30); // 获取字段的值 Object value = field.get(person); System.out.println("Value of " + fieldName + ": " + value); } } catch (Exception e) { e.printStackTrace(); } } }
输出结果
运行该代码后,输出如下所示:
Field Name: name, Field Type: java.lang.String Value of name: John Doe Field Name: age, Field Type: int Value of age: 30
注意事项
- 访问控制:对于私有字段,需要调用
setAccessible(true)
来允许访问。 - 异常处理:在使用反射时,可能会遇到多种异常,例如
ClassNotFoundException
、NoSuchFieldException
、IllegalAccessException
等,所以需要进行适当的异常处理。 - 性能:过度使用反射可能会导致性能下降,通常应在必要时使用。
通过上述步骤和示例,您可以轻松获取类的成员变量。使用反射时要谨慎,确保理解其影响和使用场景
2.3 获取类的方法
使用反射机制可以动态地获取类的信息,包括类的方法。下面是如何使用反射获取类的方法的步骤和示例代码。
步骤
-
获取
Class
对象:首先,需要获取目标类的Class
对象。 -
使用反射获取方法:
- 使用
getMethods()
方法获取所有公有的方法(包括继承的方法)。 - 使用
getDeclaredMethods()
方法获取所有声明的方法(包括私有方法)。
- 使用
-
遍历方法:遍历获取的方法,并打印出相关的信息,例如方法名、返回类型和参数类型。
示例
以下是一个示例代码,展示了如何获取某个类的方法信息:
import java.lang.reflect.Method; public class ReflectionExample { public static void main(String[] args) { // 示例类 Class<?> clazz = SampleClass.class; // 获取SampleClass的Class对象 // 获取所有公有的方法 Method[] methods = clazz.getMethods(); System.out.println("公有方法:"); for (Method method : methods) { System.out.println("方法名: " + method.getName()); System.out.println("返回类型: " + method.getReturnType().getName()); System.out.print("参数类型: "); Class<?>[] parameterTypes = method.getParameterTypes(); for (Class<?> paramType : parameterTypes) { System.out.print(paramType.getName() + " "); } System.out.println("n"); } // 获取所有声明的方法,包括私有方法 Method[] declaredMethods = clazz.getDeclaredMethods(); System.out.println("所有声明的方法:"); for (Method declaredMethod : declaredMethods) { System.out.println("方法名: " + declaredMethod.getName()); System.out.println("返回类型: " + declaredMethod.getReturnType().getName()); System.out.print("参数类型: "); Class<?>[] parameterTypes = declaredMethod.getParameterTypes(); for (Class<?> paramType : parameterTypes) { System.out.print(paramType.getName() + " "); } System.out.println("n"); } } } // 示例类 class SampleClass { public void publicMethod(String param) { // 方法实现 } private void privateMethod() { // 方法实现 } protected int protectedMethod(int x) { return x * 2; } void defaultMethod() { // 方法实现 } }
2.4 获得类的构造函数
使用反射机制可以获取类的构造函数。下面是获取构造函数的步骤和示例代码。
步骤
-
获取
Class
对象:获取目标类的Class
对象。 -
使用反射获取构造函数:
- 使用
getConstructors()
方法获取所有公有构造函数。 - 使用
getDeclaredConstructors()
方法获取所有声明的构造函数(包括私有构造函数)。
- 使用
-
遍历构造函数:遍历获取的构造函数,打印出构造函数的信息,例如名称和参数类型。
示例
import java.lang.reflect.Constructor; public class ReflectionExample { public static void main(String[] args) { // 示例类 Class<?> clazz = SampleClass.class; // 获取SampleClass的Class对象 // 获取所有公有构造函数 Constructor<?>[] constructors = clazz.getConstructors(); System.out.println("公有构造函数:"); for (Constructor<?> constructor : constructors) { System.out.println("构造函数名: " + constructor.getName()); System.out.print("参数类型: "); Class<?>[] parameterTypes = constructor.getParameterTypes(); for (Class<?> paramType : parameterTypes) { System.out.print(paramType.getName() + " "); } System.out.println("n"); } // 获取所有声明的构造函数,包括私有构造函数 Constructor<?>[] declaredConstructors = clazz.getDeclaredConstructors(); System.out.println("所有声明的构造函数:"); for (Constructor<?> declaredConstructor : declaredConstructors) { System.out.println("构造函数名: " + declaredConstructor.getName()); System.out.print("参数类型: "); Class<?>[] parameterTypes = declaredConstructor.getParameterTypes(); for (Class<?> paramType : parameterTypes) { System.out.print(paramType.getName() + " "); } System.out.println("n"); } } } // 示例类 class SampleClass { public SampleClass() { // 默认构造函数 } public SampleClass(String name) { // 带参数的构造函数 } private SampleClass(int age) { // 私有构造函数 } }
2.5 调用类的方法
使用反射机制可以动态地调用一个类的方法。下面是实现这一功能的步骤和示例代码。
步骤
-
获取
Class
对象:获取目标类的Class
对象。 -
获取方法:
- 使用
getMethod()
方法获取公有方法。 - 使用
getDeclaredMethod()
方法获取所有声明的方法(包括私有方法)。
- 使用
-
调用方法:通过
Method
对象调用目标方法。
示例
以下是一个示例代码,展示如何使用反射调用类的方法:
import java.lang.reflect.Method; public class ReflectionExample { public static void main(String[] args) { try { // 获取SampleClass的Class对象 Class<?> clazz = SampleClass.class; // 创建SampleClass的实例 Object sampleInstance = clazz.getDeclaredConstructor().newInstance(); // 获取公有方法 Method publicMethod = clazz.getMethod("publicMethod", String.class); publicMethod.invoke(sampleInstance, "Hello, World!"); // 获取私有方法 Method privateMethod = clazz.getDeclaredMethod("privateMethod"); privateMethod.setAccessible(true); // 允许访问私有方法 privateMethod.invoke(sampleInstance); } catch (Exception e) { e.printStackTrace(); } } } // 示例类 class SampleClass { public void publicMethod(String message) { System.out.println("公有方法被调用,消息: " + message); } private void privateMethod() { System.out.println("私有方法被调用"); } }
2.6 利用反射创建数组
可以使用反射来动态创建数组。通过 java.lang.reflect.Array
类,可以创建和操作数组。以下是创建数组的步骤和示例代码。
步骤
- 获取数组的
Class
对象:使用Class.forName()
或类型名.class
获取数组元素的Class
对象。 - 使用
Array.newInstance()
创建数组:调用该方法并传入数组的Class
对象和数组的维度(长度)。
示例
以下示例展示如何使用反射创建一维和二维数组:
import java.lang.reflect.Array; public class ReflectionArrayExample { public static void main(String[] args) { try { // 创建一个一维数组,类型为Integer,长度为5 Class<?> arrayClass = Integer.class; // 数组元素类型 int length = 5; Object intArray = Array.newInstance(arrayClass, length); // 设置数组元素 for (int i = 0; i < length; i++) { Array.set(intArray, i, i * 10); // 设置数组元素 } // 打印数组内容 for (int i = 0; i < length; i++) { System.out.println(Array.get(intArray, i)); // 获取数组元素 } // 创建一个二维数组,类型为String,维度为3x2 Class<?> strArrayClass = String.class; int rows = 3; int cols = 2; Object strArray = Array.newInstance(strArrayClass, rows, cols); // 设置二维数组元素 Array.set(strArray, 0, Array.newInstance(strArrayClass, cols)); // 第一行 Array.set(Array.get(strArray, 0), 0, "Hello"); Array.set(Array.get(strArray, 0), 1, "World"); Array.set(strArray, 1, Array.newInstance(strArrayClass, cols)); // 第二行 Array.set(Array.get(strArray, 1), 0, "Java"); Array.set(Array.get(strArray, 1), 1, "Reflection"); // 打印二维数组内容 for (int i = 0; i < rows; i++) { for (int j = 0; j < cols; j++) { System.out.print(Array.get(Array.get(strArray, i), j) + " "); } System.out.println(); } } catch (Exception e) { e.printStackTrace(); } } }