一文带你吃透 Java 反射机制

一文带你吃透 Java 反射机制

在Java开发中,"反射"绝对是个让人又爱又恨的知识点。有人觉得它晦涩难懂、破坏封装,也有人靠它实现了各种灵活的功能------比如框架开发、动态配置加载。

其实反射没那么神秘,今天就给大家用最通俗的语言讲清楚:反射到底是什么、怎么用,以及反射在实际开发中的应用。

一、Java反射到底是什么?

我们先从Java的核心特性"封装"说起。平时写代码时,我们通过new关键字创建对象,调用类的方法、访问属性,都是在"编译期"就确定好要操作的类,比如User user = new User();,编译器早就知道我们要操作User类。

而反射机制,简单说就是程序在运行时,能够"看透"一个类的内部结构:知道它有哪些属性、哪些方法、哪些构造器,还能动态地创建对象、调用方法、修改属性,哪怕这些成员是私有的。

形象点说,普通方式是"先知道类,再用类";反射是"先拿到类的'说明书',再根据说明书用类"。这个"说明书",就是Java中的Class类对象。

Java中的反射,本质上是运用了:类是由JVM在执行过程中动态加载的这一原理实现的。

二、Class类是关键

在Java中,任何一个类被加载后,JVM都会为它创建一个唯一的Class对象,这个对象包含了该类的所有信息(成员变量、构造方法、成员方法等)。反射的所有操作,本质上都是通过操作这个Class对象实现的。

简单梳理反射的核心流程:

  1. 获取目标类的Class对象(拿到"说明书");

  2. 通过Class对象获取需要的成员(属性、方法、构造器);

  3. 动态操作这些成员(创建对象、调用方法、修改属性)。

三、反射的基础用法

先铺垫基础用法,后面的案例会基于这些操作展开。我们以一个简单的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)中非常常见,核心就是反射。

实现步骤:

  1. 创建配置文件(比如reflect.properties),写入要加载的类名和方法名;

  2. 通过IO流读取配置文件中的类名和方法名;

  3. 用反射动态加载类、创建对象、执行方法。

步骤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类,只需要修改配置文件中的classNamemethodName,不用修改代码就能实现,这就是反射的"动态性"价值。

案例2:实现isClassPresent方法,优先加载指定类,失败则加载默认类

我们在开发中经常会遇到"降级策略"------优先加载某个指定的类,如果该类不存在(比如依赖包未引入),就加载默认的兜底类。用反射可以轻松实现这个逻辑,定义一个isClassPresent方法来判断类是否存在并加载。

实现思路:

  1. 定义一个方法,参数为classNamedefaultClassName

  2. 尝试用Class.forName()加载指定类,若不抛异常则说明类存在,返回该类的Class对象;

  3. 若加载指定类抛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反射时保持谨慎。

掌握反射,本质上是打通了"使用框架"到"理解框架底层原理"的关键一环。

如果觉得有用,欢迎点赞、在看、转发三连~ 有疑问也可以在评论区留言交流~

更多精彩文章,欢迎关注我的公众号:前端架构师笔记

相关推荐
wasp5202 小时前
AgentScope Java 核心架构深度解析
java·开发语言·人工智能·架构·agentscope
2501_916766542 小时前
【Springboot】数据层开发-数据源自动管理
java·spring boot·后端
半夏知半秋2 小时前
docker常用指令整理
运维·笔记·后端·学习·docker·容器
程序员码歌2 小时前
短思考第263天,每天复盘10分钟,胜过盲目努力一整年
android·前端·后端
自在极意功。2 小时前
MyBatis 动态 SQL 详解:从基础到进阶实战
java·数据库·mybatis·动态sql
软件管理系统2 小时前
基于Spring Boot的便民维修管理系统
java·spring boot·后端
源代码•宸3 小时前
Leetcode—620. 有趣的电影&&Q3. 有趣的电影【简单】
数据库·后端·mysql·算法·leetcode·职场和发展
百***78753 小时前
Step-Audio-2 轻量化接入全流程详解
android·java·gpt·php·llama