一文带你吃透 Java 反射机制
在Java开发中,"反射"绝对是个让人又爱又恨的知识点。有人觉得它晦涩难懂、破坏封装,也有人靠它实现了各种灵活的功能------比如框架开发、动态配置加载。
其实反射没那么神秘,今天就给大家用最通俗的语言讲清楚:反射到底是什么、怎么用,以及反射在实际开发中的应用。
一、Java反射到底是什么?
我们先从Java的核心特性"封装"说起。平时写代码时,我们通过new关键字创建对象,调用类的方法、访问属性,都是在"编译期"就确定好要操作的类,比如User user = new User();,编译器早就知道我们要操作User类。
而反射机制,简单说就是程序在运行时,能够"看透"一个类的内部结构:知道它有哪些属性、哪些方法、哪些构造器,还能动态地创建对象、调用方法、修改属性,哪怕这些成员是私有的。
形象点说,普通方式是"先知道类,再用类";反射是"先拿到类的'说明书',再根据说明书用类"。这个"说明书",就是Java中的Class类对象。
Java中的反射,本质上是运用了:类是由JVM在执行过程中动态加载的这一原理实现的。
二、Class类是关键
在Java中,任何一个类被加载后,JVM都会为它创建一个唯一的Class对象,这个对象包含了该类的所有信息(成员变量、构造方法、成员方法等)。反射的所有操作,本质上都是通过操作这个Class对象实现的。
简单梳理反射的核心流程:
-
获取目标类的
Class对象(拿到"说明书"); -
通过
Class对象获取需要的成员(属性、方法、构造器); -
动态操作这些成员(创建对象、调用方法、修改属性)。
三、反射的基础用法
先铺垫基础用法,后面的案例会基于这些操作展开。我们以一个简单的User类为例:
java
package com.example.demo;
public class User {
private String name;
private int age;
// 无参构造
public User() {}
// 有参构造
public User(String name, int age) {
this.name = name;
this.age = age;
}
// 普通方法
public void sayHello() {
System.out.println("Hello, " + name + "! You are " + age + " years old.");
}
}
第一步:获取Class对象
在Java中总共有三种方式获取Class对象:
java
// 方式1:通过类名.class(编译期确定,最安全)
Class<User> userClass1 = User.class;
// 方式2:通过对象.getClass()(运行时获取,需先有对象)
User user = new User();
Class<? extends User> userClass2 = user.getClass();
// 方式3:通过Class.forName("全类名")(动态加载,最灵活,常用)
Class<?> userClass3 = Class.forName("com.example.demo.User"); // 全类名=包名+类名
其中,第三种方式最灵活,因为全类名可以来自配置文件、数据库等,实现"动态指定类",稍后我们会详细介绍这种方式。
第二步:通过Class对象创建对象
java
// 1. 通过无参构造创建(最常用)
Class<?> userClass = Class.forName("com.example.demo.User");
User user1 = (User) userClass.newInstance();
// 2. 通过有参构造创建
Constructor<?> constructor = userClass.getConstructor(String.class, int.class);
User user2 = (User) constructor.newInstance("张三", 20);
这两种反射创建 User 实例的方式核心区别在于:前者调用无参构造器创建实例,且该方式在 Java 9 被标记为过时(@Deprecated),Java 11 彻底移除,不推荐使用;后者通过指定有参构造器创建实例,能直接传入参数完成初始化,是反射创建实例的标准推荐写法。
第三步:调用类的方法
java
// 1. 获取sayHello方法(无参、public)
Method sayHelloMethod = userClass.getMethod("sayHello");
// 2. 调用方法(需要传入对象实例)
sayHelloMethod.invoke(user2); // 输出:Hello, 张三
// 3. 调用带参方法(比如setName)
Method setNameMethod = userClass.getMethod("setName", String.class);
setNameMethod.invoke(user2, "李四");
sayHelloMethod.invoke(user2); // 输出:Hello, 李四
如果要操作私有成员(比如private属性name),需要先调用setAccessible(true)打破封装限制,这里就不展开说明了。
四、反射的两个经典应用场景
理解了基础用法,再看两个实际开发中常用的案例,帮助我们更加深刻的理解反射存在的意义。
案例1:读取配置文件,动态加载类并执行方法
我们希望程序不修改代码,只修改配置文件,就能加载不同的类、执行不同的方法,这在框架开发(比如Spring)中非常常见,核心就是反射。
实现步骤:
-
创建配置文件(比如reflect.properties),写入要加载的类名和方法名;
-
通过IO流读取配置文件中的类名和方法名;
-
用反射动态加载类、创建对象、执行方法。
步骤1:创建配置文件(reflect.properties)
properties
# 全类名
className=com.example.demo.User
# 要执行的方法名
methodName=sayHello
步骤2:编写工具类读取配置文件
java
package com.example.demo;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
public class PropertiesUtil {
public static Properties getProperties() {
Properties properties = new Properties();
InputStream is = PropertiesUtil.class.getClassLoader().getResourceAsStream("reflect.properties");
try {
properties.load(is);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (is != null) is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return properties;
}
}
步骤3:用反射动态加载并执行
java
package com.example.demo;
import java.lang.reflect.Method;
import java.util.Properties;
import java.lang.reflect.Constructor;
public class ReflectDemo1 {
public static void main(String[] args) throws Exception {
// 1. 读取配置文件
Properties properties = PropertiesUtil.getProperties();
String className = properties.getProperty("className");
String methodName = properties.getProperty("methodName");
try {
// 2. 获取Class对象
Class<?> clazz = Class.forName(className);
// 3. 创建对象
Constructor<?> constructor = clazz.getConstructor(String.class, int.class);
Object obj = constructor.newInstance("张三", 18);
// 4. 获取方法并执行
Method method = clazz.getMethod(methodName);
method.invoke(obj); // 执行sayHello方法
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行程序,会执行User类的sayHello方法。如果我们想换一个类执行,比如创建一个Order类,只需要修改配置文件中的className和methodName,不用修改代码就能实现,这就是反射的"动态性"价值。
案例2:实现isClassPresent方法,优先加载指定类,失败则加载默认类
我们在开发中经常会遇到"降级策略"------优先加载某个指定的类,如果该类不存在(比如依赖包未引入),就加载默认的兜底类。用反射可以轻松实现这个逻辑,定义一个isClassPresent方法来判断类是否存在并加载。
实现思路:
-
定义一个方法,参数为
className和defaultClassName; -
尝试用
Class.forName()加载指定类,若不抛异常则说明类存在,返回该类的Class对象; -
若加载指定类抛
ClassNotFoundException,则加载默认类并返回其Class对象。
步骤1:创建默认类和备选类
java
// 默认兜底类
public class DefaultService {
public void doService() {
System.out.println("执行默认服务逻辑");
}
}
// 备选指定类(可能不存在)
public class CustomService {
public void doService() {
System.out.println("执行自定义服务逻辑");
}
}
步骤2:实现isClassPresent方法
java
package com.example.demo;
public class ClassLoaderUtil {
/**
* 优先加载指定类,失败则加载默认类
* @param className 要优先加载的类全类名
* @param defaultClassName 兜底的默认类全类名
* @return 加载成功的Class对象
* @throws ClassNotFoundException 若默认类也不存在则抛异常
*/
public static Class<?> isClassPresent(String className, String defaultClassName) throws ClassNotFoundException {
try {
// 优先加载指定类
return Class.forName(className);
} catch (ClassNotFoundException e) {
System.out.println("指定类[" + className + "]不存在,加载默认类[" + defaultClassName + "]");
// 加载失败,加载默认类
return Class.forName(defaultClassName);
}
}
}
步骤3:测试验证
java
package com.example.demo;
import java.lang.reflect.Method;
import java.lang.reflect.Constructor;
public class ReflectDemo2 {
public static void main(String[] args) throws Exception {
// 场景1:指定类存在(CustomService已创建)
Class<?> clazz1 = ClassLoaderUtil.isClassPresent(
"com.example.demo.CustomService",
"com.example.demo.DefaultService"
);
Constructor<?> constructor1 = clazz1.getConstructor();
Object obj1 = constructor1.newInstance();
Method method1 = clazz1.getMethod("doService");
method1.invoke(obj1); // 输出:执行自定义服务逻辑
// 场景2:指定类不存在(故意写一个错误的类名)
Class<?> clazz2 = ClassLoaderUtil.isClassPresent(
"com.example.demo.NonExistentService", // 不存在的类
"com.example.demo.DefaultService"
);
Constructor<?> constructor2 = clazz2.getConstructor();
Object obj2 = constructor2.newInstance();
Method method2 = clazz2.getMethod("doService");
method2.invoke(obj2); // 输出:指定类[com.example.NonExistentService]不存在,加载默认类[com.example.DefaultService] + 执行默认服务逻辑
}
}
这个逻辑在实际开发中很实用,比如:
-
框架的插件化开发:优先加载用户自定义的插件类,没有则用默认实现;
-
依赖降级:当某个第三方依赖包未引入时,自动切换到本地默认实现,避免程序崩溃。
五、写在最后
Java的反射,打破了Java程序在编译期的束缚,能在运行时动态加载类、执行方法,既大幅提升了程序的灵活性与扩展性,也减少了类间的硬编码依赖。更关键的是,反射是诸多Java核心技术的基石,比如:Spring IOC、MyBatis Mapper映射、JUnit单元测试框架等,底层都离不开它的支撑。
但我们也不能忽视它的"另一面",反射对私有成员的访问会破坏面向对象的封装原则,解析Class对象、验证权限等操作带来的性能开销,这就要求我们在使用Java反射时保持谨慎。
掌握反射,本质上是打通了"使用框架"到"理解框架底层原理"的关键一环。
如果觉得有用,欢迎点赞、在看、转发三连~ 有疑问也可以在评论区留言交流~
更多精彩文章,欢迎关注我的公众号:前端架构师笔记