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