40. 【Java】反射:窥探类的“内在”

摘要: 本文深入解析Java反射机制的核心原理与实践应用,从获取Class对象到动态访问字段、方法和构造方法,全面讲解反射在运行时检查与操作类内部结构的能力。通过实际代码示例展示反射在框架开发、序列化、配置驱动等场景中的关键作用,并探讨反射的性能开销、安全限制以及与JDK 21模块化的协同注意事项,帮助开发者掌握这一Java动态编程的强大工具。

关键词: Java反射, Class对象, 动态调用, 字段访问, 方法调用


在之前的文章中,我们一直在做一件事:写代码,然后编译运行。你定义一个类,写下它的字段和方法,然后创建对象,调用方法——这一切都在“编译时”就已经确定了。

但你想过没有:有没有可能在程序运行时,去“检查”一个类的内部结构?

比如,你有一个对象,但你不知道它是什么类型的。你能在运行时问它:“你是什么类?你有什么字段?有哪些方法?我能调用你的方法吗?”

这种能力,在 Java 中叫做 反射(Reflection)

反射就像是一台“显微镜”或“X光机”,它让你在程序运行时,能够透视任意一个类的内部结构——无论这个类是 JDK 自带的,还是你自己写的,甚至是第三方库里的。

现代 Java 的很多基础设施都建立在反射之上:

  • Spring 框架:用反射读取你写的 @Service@Autowired 注解,动态创建和管理对象。
  • JUnit 测试框架:用反射找到所有标注了 @Test 的方法,并动态调用它们。
  • JSON 序列化库(如 Jackson、Gson):用反射读取对象的字段名和值,转换成 JSON 字符串。
  • IDE 的自动补全和智能提示:也是通过反射(或更底层的机制)来分析代码结构。

今天,我们就来学习反射这个强大的工具。虽然你可能不会每天都在代码里直接用反射(它确实有点“重量级”),但理解它,能让你更深刻地理解 Java 的动态性,也让你在阅读框架源码时不再一头雾水。


1. 什么是反射?—— 运行时的“解剖刀”

反射(Reflection) 是 Java 语言的一种能力,它允许运行中的程序检查和操作类、接口、字段、方法的内部信息,而不需要依赖编译时的类型信息。

通俗地说:反射让你在程序运行时,能够“扒开”一个对象的外壳,看看它里面到底有什么,甚至可以动态地调用它的方法、修改它的字段——即使这些字段和方法是 private 的。

打个比方:你平时开车,是通过方向盘、油门、刹车来操控汽车——这是“正常调用”。而反射就像是,你把引擎盖打开,用工具直接去拧里面的螺丝、拉动里面的线路——这是“底层操作”。正常情况下你不应该这么做,但有时候(比如修车,即框架开发)你确实需要这种能力。


2. 获取 Class 对象 —— 反射的入口

要使用反射,你首先需要获得一个类的 Class 对象Class 对象是反射的“总入口”,它包含了类的所有元数据——类名、包名、父类、字段、方法、构造方法、注解等等。

Java 提供了三种方式获取 Class 对象:

① 通过类名 .class
Class<String> clazz1 = String.class;
Class<Student> clazz2 = Student.class;

这种方式最简单,不需要创建对象。

② 通过对象的 getClass() 方法
Student s = new Student("张三", 20, "S001");
Class<?> clazz = s.getClass();  // 运行时实际类型

这种方式适合你不知道对象具体类型的情况。

③ 通过 Class.forName(String className)
try {
    Class<?> clazz = Class.forName("java.util.ArrayList");
    Class<?> clazz2 = Class.forName("com.example.demo.Student");
} catch (ClassNotFoundException e) {
    // 类不存在时抛出
}

这种方式适合从配置文件中读取类名,动态加载。这是很多框架(如 JDBC 驱动加载)的经典用法。

注意:Class.forName() 会触发类的静态初始化(执行 static 块),而前两种方式不会。


3. 通过反射获取类的基本信息

有了 Class 对象,你可以获取类的各种元信息:

import java.lang.reflect.Modifier;

public class ReflectionDemo {
    public static void main(String[] args) {
        Class<?> clazz = Student.class;

        // 1. 类名
        System.out.println("类名:" + clazz.getName());           // 全限定名:com.example.Student
        System.out.println("简单类名:" + clazz.getSimpleName()); // Student

        // 2. 包名
        System.out.println("包名:" + clazz.getPackage().getName());

        // 3. 父类
        Class<?> superClass = clazz.getSuperclass();
        System.out.println("父类:" + (superClass != null ? superClass.getSimpleName() : "无"));

        // 4. 实现的接口
        Class<?>[] interfaces = clazz.getInterfaces();
        System.out.println("实现的接口:");
        for (Class<?> iface : interfaces) {
            System.out.println("  " + iface.getSimpleName());
        }

        // 5. 修饰符
        int modifiers = clazz.getModifiers();
        System.out.println("是否是 public:" + Modifier.isPublic(modifiers));
        System.out.println("是否是 abstract:" + Modifier.isAbstract(modifiers));
        System.out.println("是否是 final:" + Modifier.isFinal(modifiers));
    }
}

4. 通过反射访问字段

你可以通过反射获取类的所有字段(包括 private 的),并读取或修改它们的值。

4.1 获取字段
import java.lang.reflect.Field;

public class FieldReflectionDemo {
    public static void main(String[] args) throws Exception {
        Class<?> clazz = Student.class;

        // 1. 获取所有 public 字段(包括继承的)
        Field[] publicFields = clazz.getFields();

        // 2. 获取所有字段(包括 private、protected,但不包括继承的)
        Field[] allFields = clazz.getDeclaredFields();
        for (Field field : allFields) {
            System.out.println("字段名:" + field.getName());
            System.out.println("  类型:" + field.getType().getSimpleName());
            System.out.println("  修饰符:" + Modifier.toString(field.getModifiers()));
        }

        // 3. 获取特定字段(按名称)
        Field nameField = clazz.getDeclaredField("name");  // 假设 Student 有 name 字段
        System.out.println("找到字段:name,类型:" + nameField.getType().getSimpleName());
    }
}
4.2 读取和修改字段的值
public class FieldAccessDemo {
    public static void main(String[] args) throws Exception {
        // 创建对象
        Student s = new Student("张三", 20, "S001");
        Class<?> clazz = s.getClass();

        // 获取私有字段 name
        Field nameField = clazz.getDeclaredField("name");
        // 重要!访问私有字段需要设置可访问
        nameField.setAccessible(true);

        // 读取值
        String name = (String) nameField.get(s);
        System.out.println("原来姓名:" + name);

        // 修改值
        nameField.set(s, "李四");
        System.out.println("修改后姓名:" + s.getName());  // 李四

        // 获取 age 字段
        Field ageField = clazz.getDeclaredField("age");
        ageField.setAccessible(true);
        ageField.setInt(s, 25);  // 用 setInt 设置 int 类型
        System.out.println("修改后年龄:" + s.getAge());
    }
}

重要:调用 setAccessible(true) 可以绕过 privateprotected 的访问限制。这是反射最强大的地方之一,也是最需要谨慎使用的功能——它破坏了封装性,可能带来安全隐患。


5. 通过反射访问方法

你可以获取类的所有方法,并动态调用它们(即使方法是 private 的)。

5.1 获取方法
import java.lang.reflect.Method;

public class MethodReflectionDemo {
    public static void main(String[] args) {
        Class<?> clazz = Student.class;

        // 1. 获取所有 public 方法(包括继承的)
        Method[] publicMethods = clazz.getMethods();

        // 2. 获取所有方法(包括 private,不包括继承的)
        Method[] allMethods = clazz.getDeclaredMethods();
        for (Method method : allMethods) {
            System.out.println("方法名:" + method.getName());
            System.out.println("  返回类型:" + method.getReturnType().getSimpleName());
            System.out.println("  参数类型:");
            for (Class<?> param : method.getParameterTypes()) {
                System.out.println("    " + param.getSimpleName());
            }
            System.out.println("  修饰符:" + Modifier.toString(method.getModifiers()));
            System.out.println("---");
        }

        // 3. 获取特定方法(需要指定方法名和参数类型)
        Method sayHello = clazz.getDeclaredMethod("sayHello");  // 无参方法
        Method setName = clazz.getDeclaredMethod("setName", String.class);  // 有参方法
    }
}
5.2 动态调用方法
public class MethodInvokeDemo {
    public static void main(String[] args) throws Exception {
        // 创建对象
        Student s = new Student("张三", 20, "S001");
        Class<?> clazz = s.getClass();

        // 1. 调用 public 方法
        Method sayHello = clazz.getDeclaredMethod("sayHello");
        sayHello.invoke(s);  // 相当于 s.sayHello()

        // 2. 调用带参数的方法
        Method setName = clazz.getDeclaredMethod("setName", String.class);
        setName.invoke(s, "王五");  // 相当于 s.setName("王五")
        System.out.println("修改后:" + s.getName());

        // 3. 调用 private 方法(需要 setAccessible)
        Method privateMethod = clazz.getDeclaredMethod("privateHelper");  // 假设有这样一个私有方法
        privateMethod.setAccessible(true);
        Object result = privateMethod.invoke(s);  // 如果有返回值
        System.out.println("私有方法返回值:" + result);

        // 4. 调用静态方法(传入 null 作为对象)
        Method staticMethod = clazz.getDeclaredMethod("staticMethod", String.class);
        staticMethod.invoke(null, "Hello");  // 相当于 ClassName.staticMethod("Hello")
    }
}

Method.invoke(Object obj, Object... args) 的第一个参数是调用方法的对象(静态方法传 null),后面的参数是方法参数。


6. 通过反射操作构造方法

你可以用反射动态创建对象,而不使用 new 关键字。

import java.lang.reflect.Constructor;

public class ConstructorReflectionDemo {
    public static void main(String[] args) throws Exception {
        Class<?> clazz = Student.class;

        // 1. 获取所有构造方法
        Constructor<?>[] constructors = clazz.getDeclaredConstructors();
        for (Constructor<?> c : constructors) {
            System.out.println("构造方法:" + c.getName());
            System.out.println("  参数类型:");
            for (Class<?> param : c.getParameterTypes()) {
                System.out.println("    " + param.getSimpleName());
            }
        }

        // 2. 获取指定构造方法
        Constructor<?> c1 = clazz.getDeclaredConstructor(String.class, int.class, String.class);
        // 如果构造方法是 private,需要 setAccessible
        // 但通常是 public 的

        // 3. 用构造方法创建对象
        Student s = (Student) c1.newInstance("张三", 20, "S001");
        System.out.println("通过反射创建的学生:" + s.getName());

        // 4. 获取无参构造方法
        Constructor<?> c2 = clazz.getDeclaredConstructor();
        Student s2 = (Student) c2.newInstance();
        System.out.println("通过无参构造创建:" + s2);
    }
}

7. 反射与泛型 —— 类型擦除后的“残余信息”

我们之前学过,Java 的泛型有类型擦除——运行时,List<String>List<Integer> 在 JVM 看来都是 List。但反射可以捕获到泛型的“残余信息”,比如通过 Field.getGenericType() 获取带泛型参数的类型。

import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.List;

public class GenericReflectionDemo {
    private List<String> names;

    public static void main(String[] args) throws Exception {
        Field field = GenericReflectionDemo.class.getDeclaredField("names");
        
        // 普通类型
        Class<?> fieldType = field.getType();
        System.out.println("普通类型:" + fieldType.getSimpleName());  // List

        // 泛型类型(保留泛型信息)
        Type genericType = field.getGenericType();
        if (genericType instanceof ParameterizedType) {
            ParameterizedType pt = (ParameterizedType) genericType;
            System.out.println("原始类型:" + pt.getRawType().getTypeName());  // List
            for (Type arg : pt.getActualTypeArguments()) {
                System.out.println("泛型参数:" + arg.getTypeName());  // String
            }
        }
    }
}

这种能力在 JSON 解析框架(如 Jackson)中非常关键——它需要知道 List<User> 里的元素是 User 类型,才能正确反序列化。


8. 反射的性能与安全

8.1 性能开销

反射比普通调用,原因包括:

  • Class 对象的创建和元数据的加载
  • setAccessible 的检查
  • 方法的动态分派
  • 编译器的优化受限

在性能敏感的代码中(比如每秒调用数百万次),应避免使用反射。但在大多数业务场景(如框架初始化、配置加载)中,反射的开销可以忽略。

粗略经验:反射调用比直接调用慢 2~5 倍。但如果你只调用一次或几次,完全不用担心。

8.2 安全限制

反射可以绕过 private 修饰符,这破坏了封装性。在安全管理器(SecurityManager)启用的情况下,某些反射操作会被禁止(比如访问 private 字段)。在大多数普通 Java 应用中,安全管理器默认是关闭的。


9. 反射的典型应用场景

场景说明
框架开发Spring、JUnit、Hibernate 等通过反射读取注解、动态创建对象、注入依赖。
序列化/反序列化JSON/XML 库通过反射读取对象的字段名和值。
配置驱动的代码从配置文件读取类名,用 Class.forName() 动态加载。
调试工具IDE 的变量查看器、JVM 的 JMX 工具等通过反射读取运行时对象的内部状态。
注解处理器读取注解并进行处理(比如生成代码)。
ORM 框架将数据库表映射到 Java 对象,通过反射读取字段并赋值。

10. 综合示例 —— 一个简单的“对象转 JSON”工具

我们写一个小工具,把任意对象的字段(包括私有)转成 JSON 格式的字符串。

import java.lang.reflect.Field;
import java.util.*;

public class SimpleJsonSerializer {

    /**
     * 将对象序列化为 JSON 字符串(简化版,只处理基本类型和 String)
     */
    public static String toJson(Object obj) throws IllegalAccessException {
        if (obj == null) {
            return "null";
        }

        Class<?> clazz = obj.getClass();

        // 如果是基本类型、String、包装类,直接返回字符串表示
        if (clazz.isPrimitive() || obj instanceof String || 
            obj instanceof Number || obj instanceof Boolean) {
            return String.valueOf(obj);
        }

        // 如果是 List/Set/数组,处理成 JSON 数组
        if (obj instanceof Collection<?> collection) {
            List<String> items = new ArrayList<>();
            for (Object item : collection) {
                items.add(toJson(item));
            }
            return "[" + String.join(",", items) + "]";
        }

        // 如果是 Map,处理成 JSON 对象
        if (obj instanceof Map<?, ?> map) {
            List<String> entries = new ArrayList<>();
            for (Map.Entry<?, ?> entry : map.entrySet()) {
                String key = String.valueOf(entry.getKey());
                String value = toJson(entry.getValue());
                entries.add("\"" + key + "\":" + value);
            }
            return "{" + String.join(",", entries) + "}";
        }

        // 普通对象:处理所有字段
        Field[] fields = clazz.getDeclaredFields();
        Map<String, String> jsonMap = new LinkedHashMap<>();

        for (Field field : fields) {
            // 跳过静态字段
            if (java.lang.reflect.Modifier.isStatic(field.getModifiers())) {
                continue;
            }

            field.setAccessible(true);
            Object value = field.get(obj);
            jsonMap.put(field.getName(), toJson(value));
        }

        // 构建 JSON 字符串
        List<String> pairs = new ArrayList<>();
        for (Map.Entry<String, String> entry : jsonMap.entrySet()) {
            pairs.add("\"" + entry.getKey() + "\":" + entry.getValue());
        }
        return "{" + String.join(",", pairs) + "}";
    }

    public static void main(String[] args) throws Exception {
        // 测试用的数据类
        class Address {
            String city = "成都";
            String street = "天府大道";
        }

        class Person {
            String name = "张三";
            int age = 20;
            boolean active = true;
            Address address = new Address();
            List<String> hobbies = List.of("编程", "阅读", "跑步");
        }

        Person person = new Person();
        String json = toJson(person);
        System.out.println("JSON 输出:");
        System.out.println(json);

        // 输出(格式化后):
        // {
        //   "name":"张三",
        //   "age":20,
        //   "active":true,
        //   "address":{
        //     "city":"成都",
        //     "street":"天府大道"
        //   },
        //   "hobbies":["编程","阅读","跑步"]
        // }
    }
}

这个例子展示了反射在序列化场景中的实际应用——我们不需要为每个类单独写序列化方法,反射可以通用地处理任何对象。


11. 反射与 JDK 21 的协同

JDK 21 中,反射依然是最重要的底层机制之一。但需要注意的是,随着模块化(JPMS)的普及,反射在模块间的访问受到了一些限制:

  • 如果一个包没有被 exports 导出,外部模块的反射无法访问它。
  • 如果一个包被 opens 开放,则允许反射访问(即使没有导出)。

module-info.java 中声明:

module com.example.demo {
    // 导出包(正常访问)
    exports com.example.demo.api;
    
    // 开放包给反射
    opens com.example.demo.model;
}

这意味着,在模块化项目中,反射不是“无限制”的,框架开发者需要明确声明哪些包可以被反射访问。你作为应用开发者,通常不需要太担心这个问题——框架会处理好。


12. 今天的总结

今天我们学习了反射的核心内容:

  • 反射是什么:运行时检查、操作类、字段、方法的能力。
  • 获取 Class 对象类名.class对象.getClass()Class.forName()
  • 获取类信息:类名、包名、父类、接口、修饰符。
  • 操作字段getDeclaredFields()setAccessible(true)get()set()
  • 操作方法getDeclaredMethods()invoke()
  • 操作构造方法getDeclaredConstructors()newInstance()
  • 反射与泛型getGenericType() 获取泛型参数信息。
  • 性能与安全:反射有开销,可绕过 private,需谨慎使用。
  • 应用场景:框架、序列化、配置加载、调试工具。
  • JDK 21 注意点:模块化对反射有一定限制。

反射是 Java 强大动态性的体现,也是很多高级框架的基石。虽然你可能不会每天都直接用它,但了解它,会让你在遇到“运行时动态处理”的问题时,多一把有力的武器。


动手试试

  1. 写一个方法 printClassInfo(Class<?> clazz),打印类的所有信息:类名、父类、接口、所有字段(含类型和修饰符)、所有方法(含返回类型、参数和修饰符)。

  2. 用反射创建一个 ArrayList 对象,添加几个元素,然后遍历并打印这些元素(注意 ArrayList 的泛型被擦除了,但你可以通过反射调用 add 方法)。

  3. 给定一个对象,用反射遍历它的所有字段,把所有 String 类型的字段值改为 "modified"(提示:需要检查 field.getType() == String.class)。

  4. (挑战)写一个 @Log 注解(运行时保留),用反射读取标注了这个注解的方法,并在方法执行前后打印日志。你需要用反射实现一个简单的“AOP”效果。

反射的内容就讲到这里了。下一篇文章,我们将学习注解——那些给代码“贴标签”的元数据机制,它是很多框架(包括 Spring)的核心工具。

我们下一篇见。😃

📌 获取本系列示例代码请访问 GitCode

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值