不是说反射会影响性能吗,那为什么 Java 中有那么多地方还用反射,不用不行吗?
答案是:不行,除非 Java 提供了更好的方法。
什么是反射
不用多说,反射是 Java 学习中逃不掉的概念,属于进阶一点的知识点。
有人说,我不会反射,而且平时也根本不用反射呀。没错,大多数情况下,你不主动用它也能完成绝大多数开发任务。
这其中有一个很重要的隐藏信息,就是现在用的框架都封装的非常好,让你感觉不到你在用反射,而实际框架的内部,有很多地方要依靠反射的能力。
好了,先用简单的几点概括一下反射是什么:
运行时自省:允许程序在运行时检查自身的结构和状态。 动态访问:能够在运行时访问类、方法、字段等,而不需要在编译时知道它们的名称。 类型操作:提供了在运行时操作类型信息的能力,包括创建实例、调用方法、修改字段值等。 打破封装:允许访问和修改私有成员,从而在必要时突破常规的访问限制。 元数据处理:支持在运行时读取和处理注解等元数据信息。 动态加载:能够在运行时动态加载和使用类。 灵活性增强:为程序提供了极大的灵活性,使得可以编写更通用、可扩展的代码。
反射的用法
Java 反射主要涉及以下核心类,它们都位于 java.lang.reflect
包中,具体用法不用多说,复制粘贴,说来就来。
这里举一个最简单的例子:
// 获取类的 Class 对象
Class<?> clazz = MyClass.class;
// 创建实例
Object obj = clazz.newInstance();
// 获取方法
Method method = clazz.getMethod("myMethod", String.class);
// 调用方法
method.invoke(obj, "Hello, Reflection!");
// 获取字段
Field field = clazz.getDeclaredField("myField");
// 设置字段可访问(如果是私有的)
field.setAccessible(true);
// 设置字段值
field.set(obj, "New Value");
反射的作用
反射的作用全都围绕运行时展开,我们知道,一个程序主要包括编译期和运行时两个大阶段。
编译期会检查语言能确定的部分,比如语法是否正确,以及事先已知的操作,比如要访问一个 User
对象的 name
属性。
而到了运行时,代码已经固定了,这时候再想对已有程序做一些修改和扩展,最常见的方法就是改代码,然后重新编译,重新运行。
而很多情况下,这是不可预见的。举个最经典的例子,就是数据库驱动的例子,尤其是我们用 Spring Boot或Spring 开发,都会把数据库配置放到配置文件中。在程序开始运行之前,应用程序是不知道到底配置了哪种数据库的,是 MySQL 还是 PostgreSQL,都是未知数,在这种情况下,在运行时动态获取当前配置的数据库信息就变得非常关键了。
String driverClassName = getConfig("db.driverClassName"); // 可能是 "com.mysql.cj.jdbc.Driver" 或其他驱动类
Class.forName(driverClassName);
好了,那反射具体能干什么呢?
运行时类型检查:可以在运行时确定对象的类型。动态加载类:可以在运行时加载、使用编译期间完全未知的类。获取类信息:可以在运行时获取类的方法、字段、构造函数等信息。创建对象实例:可以动态创建对象实例,而无需通过 new 关键字。调用方法:可以在运行时调用任意方法,包括私有方法。操作字段:可以在运行时获取或设置对象的任意字段值,包括私有字段。操作数组:可以动态创建数组,获取数组长度,读写数组元素等。
概括起来,就是能够利用反射完成对对象的操作,只不过是在运行时完成的。
反射的应用场景
那反射到底用在什么场景下呢?
说句正确的废话,当你碰到需要用到合适的场景时,自然而然就会发现反射。
我上一次主动用反射是这样的一个场景:有一个字段非常多的内容,也就一百多个吧,还好直接导入的,不用在页面上一个个填,但是展示的时候要把这一百多个字段一一展示出来。
这样的场景下,想想能用什么办法呢?
当然了,按照常规展示表单的方法,定义一个包含这一百多个属性的实体类,然后一一赋值,最后返给前端。这也不是不可以,但是这么多字段,够前后端喝一壶的了。那有没有更好的方法呢?
当然有了,反射就是不错的选择,有了反射,只需要遍历所有属性,然后对照字典表,把前端要展示的字段显示名和值返回一个列表给前端就好了,前端更省事儿了,直接循环列表。代码量和开发时间指数下降。
这个算是比较特殊的例子了,但是即便不是这样,很多 Bean 拷贝的库也都是用反射实现的。
接下来看一看反射主要的使用场景,而且是我们经常用到的。
Spring 依赖注入
依赖注入是每个 Java 开发者都绕不过去的面试题,是 Spring 框架的核心技术。
Spring 中大量使用反射来实现其依赖注入机制。
我们在使用 Spring 时,肯定在开发过程中碰到过类似于下面的这种异常,从中可以看出是进行反射调用的时候发生了异常。
java.lang.NullPointerException
sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:87)
at java.lang.reflect.Method.invoke(Method.java:292)
下面用几个简单的例子演示Spring 中是如何注入被 @Autowired
注解标识的服务类的。
public class TestContainer {
private Map<Class<?>, Object> container = new HashMap<>();
public void register(Class<?> clazz) throws Exception {
Constructor<?> constructor = clazz.getDeclaredConstructor();
Object instance = constructor.newInstance();
for (Field field : clazz.getDeclaredFields()) {
if (field.isAnnotationPresent(Autowired.class)) {
Class<?> dependencyClass = field.getType();
Object dependency = container.get(dependencyClass);
field.setAccessible(true);
field.set(instance, dependency);
}
}
container.put(clazz, instance);
}
public <T> T get(Class<T> clazz) {
return clazz.cast(container.get(clazz));
}
}
在上述代码中:
register
方法使用反射来创建类的实例。它遍历类的所有字段,检查是否有 @Autowired
注解。对于有注解的字段,使用反射来设置字段的值。
ORM 框架
ORM 框架更是把反射用到了极致,写ORM框架的人肯定是不知道将来你的数据库中有多少个表,每个表有多少字段,字段的名字都是什么。但是最终需要将数据表里的字段值映射到 Java 对象来。
下面是一个简单的示例:
public class SimpleORM {
public <T> T mapResultSetToObject(ResultSet rs, Class<T> clazz) throws Exception {
T instance = clazz.getDeclaredConstructor().newInstance();
for (Field field : clazz.getDeclaredFields()) {
if (field.isAnnotationPresent(Column.class)) {
String columnName = field.getAnnotation(Column.class).name();
Object value = rs.getObject(columnName);
field.setAccessible(true);
field.set(instance, value);
}
}
return instance;
}
}
@Retention(RetentionPolicy.RUNTIME)
@interface Column {
String name();
}
class User {
@Column(name = "id")
private int id;
@Column(name = "name")
private String name;
// getters and setters
}
mapResultSetToObject
方法使用反射创建类的实例。它遍历类的所有字段,检查是否有 @Column
注解。对于有注解的字段,它使用反射来设置字段的值,值来自 ResultSet
。
单元测试中
JUnit 等测试框架使用反射来发现和运行测试方法。我们用 JUnit 写单元测试用例的时候,会在测试方法上加上 @Test
的注解。然后项目编译的时候,会运行测试用例,测试框架是如何找到测试用例方法的呢,就是通过反射。
public class SimpleTestRunner {
public void runTests(Class<?> testClass) throws Exception {
Object testInstance = testClass.getDeclaredConstructor().newInstance();
for (Method method : testClass.getDeclaredMethods()) {
if (method.isAnnotationPresent(Test.class)) {
try {
method.invoke(testInstance);
System.out.println(method.getName() + " passed");
} catch (InvocationTargetException e) {
System.out.println(method.getName() + " failed: " + e.getCause().getMessage());
}
}
}
}
}
@Retention(RetentionPolicy.RUNTIME)
@interface Test {}
class MyTests {
@Test
public void testAddition() {
assert 1 + 1 == 2;
}
@Test
public void testSubtraction() {
assert 1 - 1 == 1; // This will fail
}
}
public class TestExample {
public static void main(String[] args) throws Exception {
SimpleTestRunner runner = new SimpleTestRunner();
runner.runTests(MyTests.class);
}
}
runTests
方法使用反射创建测试类的实例。它遍历类的所有方法,检查是否有 @Test
注解。对于有注解的方法,它使用反射来调用这些方法。
动态代理
还有就是 Java 动态代理机制,也是强依赖反射实现的。
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
interface MyInterface {
void doSomething();
}
class MyInterfaceImpl implements MyInterface {
public void doSomething() {
System.out.println("Doing something");
}
}
class MyInvocationHandler implements InvocationHandler {
private Object target;
public MyInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("Before method " + method.getName());
Object result = method.invoke(target, args);
System.out.println("After method " + method.getName());
return result;
}
}
public class DynamicProxyExample {
public static void main(String[] args) {
MyInterface original = new MyInterfaceImpl();
MyInterface proxy = (MyInterface) Proxy.newProxyInstance(
MyInterface.class.getClassLoader(),
new Class<?>[] { MyInterface.class },
new MyInvocationHandler(original)
);
proxy.doSomething();
}
}
Proxy.newProxyInstance
方法使用反射动态创建一个实现了指定接口的代理类。InvocationHandler
的invoke
方法使用反射来调用原始对象的方法。
配置管理
其实就是我们前面举的那个数据库驱动的例子,根据配置文件动态加载一些配置类。
import java.io.FileInputStream;
import java.util.Properties;
public class ConfigurationExample {
public static void main(String[] args) throws Exception {
Properties props = new Properties();
props.load(new FileInputStream("config.properties"));
String className = props.getProperty("main.class");
Class<?> clazz = Class.forName(className);
Object instance = clazz.getDeclaredConstructor().newInstance();
if (instance instanceof Runnable) {
((Runnable) instance).run();
}
}
}
我们从配置文件中读取类名。 使用 Class.forName
动态加载类。使用反射创建类的实例。 如果类实现了 Runnable
接口,我们调用其run
方法。
反射的注意事项
看了这么多例子,不得不说,反射确实是个好东西。但是,没有十全十美的存在,它还存在一些问题,需要我们权衡。
性能考虑:反射操作比直接方法调用慢,在性能敏感的场景中应谨慎使用。
安全性:反射可以访问私有成员,可能破坏封装,使用时需要注意安全便捷。
可读性:过度使用反射可能使代码难以理解和维护。
编译时类型检查:使用反射会绕过编译时类型检查,可能导致运行时错误。
异常处理:反射方法可能抛出多种异常,需要适当的异常处理。
👇🏻点击下方阅读原文,获取鱼皮的编程学习路线、原创项目教程、求职面试宝典、编程交流圈子。
往期推荐
我的新书,冲上了京东榜一!
24 年最新项目,手把手教程
我做了个闯关游戏,竟难倒了无数程序员。。
看了我的简历指南,面试率翻倍
AI 智能答题项目,保姆级教程
我的编程学习小圈子