上一篇我们花了大篇幅讲类加载机制,很多人可能会问:讲这么多类加载,跟反射到底有啥关系?

今天这篇我们就正式进入反射。读你会发现,搞懂了类加载,反射根本没有什么秘密而言。

一、反射是什么

上一篇我们已经说过,.java编译成.class,里面存了类的元数据,类名、父类、接口、字段、方法、修饰符等等信息都在里面。

类加载之后,这些信息就被放到方法区,然后在堆里面生成一个Class对象作为访问的入口。

其实反射就是在程序运行时,通过Class对象,拿到然后操作这个类的全部元数据。

反射能够知道对象是什么类型的。反射能够知道类有哪些字段、方法。

权限是private的,通过反射照样能够调用。

编译的时候不知道类名,运行时一样可以创建对象。

大白话就是我们正常写代码的时候是在正向的使用类,而反射就是在反向的拆解类。

二、反射的起点

反射的所有操作都是从Class对象开始的。这个对象不是我们new出来的实例,而是JVM在类加载的时候自动创建的,前文我们讲的,这个对象就是代表这个类的档案信息。

我们怎么才能拿到Class对象呢?

有三种方式:

我们还是用前文的Student来举例子。

第一种,直接使用类名.class获取

Class<Student> c1 = Student.class;

这种方式不会触发类的初始化,前文我们讲过类初始化的一些触发条件。

第二种,使用对象.getClass()获取

Student s1 = new Student("懒惰蜗牛", 28);
Class<? extends Student> c2 = s1.getClass();

这种方式就是先有了对象,然后通过这个对象去拿到Class对象。

有人可能会有疑问?既然都有对象了,为啥还要通过对象去获取Class对象呢?这不是脱裤子放屁吗?

下面我通过一个简单的例子做一下说明,假设我们有一把钥匙(对象),但是你想知道这把钥匙能开什么样的锁(类信息)。虽然你现在有这把钥匙了,但是通过仔细的研究钥匙的细节(getClass()),你就可以了解这把锁的所有特性信息,还可以复制出更多同类型的钥匙。

文字不好理解的话,我们上点代码:

public void processObject(Object obj) {

    Class<?> clazz = obj.getClass();
    
    Method[] methods = clazz.getDeclaredMethods();
    Field[] fields = clazz.getDeclaredFields();
    
    if (clazz.isAnnotationPresent(MyAnnotation.class)) {
        // 处理带有特定注解的对象
    }
}

假设上面是个框架的方法,可以接收任意的对象,方法里面我们只有obj这个对象,不知道他是什么类型的。但是通过反射,我们可以获取到对应的Class对象中的信息。然后通过这些信息做一些特定的处理。

如果你玩过Sping,那你就不会有这个疑惑了,因为Spring框架里面到处都是这样的场景。

所以说如果我们只是简单写一下自己的业务代码,Student s1 = new Student(...),然后马上s1.getName(),那确实没必要拿Class对象。

但是如果你已经在搞架构搞框架了,你收到一个不知道是什么类型的Object student,你需要确定到底是不是Student,有没有@Component注解,有哪些需要注入的字段,这个时候s1.getClass()就不是脱裤子放屁了。

第三种,使用全限定名获取。

Class<?> c3 = Class.forName("com.lazy.snail.day67.Student");

这种方式就会触发类初始化了。

虽然有三种方式可以获取Class对象,但是同一个类,无论用哪种方式,拿到的Class对象都是同一个。

因为类在JVM中只加载一次。

简单的写一个Demo验证一下:

package com.lazy.snail.day67;

/**
 * @ClassName Day67Demo1
 * @Description TODO
 * @Author lazysnail
 * @Date 2026/3/6 11:13
 * @Version 1.0
 */
public class Day67Demo1 {
        public static void main(String[] args) {
            // 方式1:通过类名.class
            Class<Student> c1 = Student.class;

            // 方式2:通过对象的getClass()
            Student s1 = new Student("懒惰蜗牛", 28);
            Class<? extends Student> c2 = s1.getClass();

            // 方式3:通过Class.forName()动态加载
            Class<?> c3 = null;
            try {
                c3 = Class.forName("com.lazy.snail.day67.Student");
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }

            // 验证:用 == 比较内存地址
            System.out.println("c1 == c2: " + (c1 == c2));
            System.out.println("c1 == c3: " + (c1 == c3));
            System.out.println("c2 == c3: " + (c2 == c3));

            // 验证hashCode也相同
            System.out.println("c1.hashCode(): " + c1.hashCode());
            System.out.println("c2.hashCode(): " + c2.hashCode());
            System.out.println("c3.hashCode(): " + c3.hashCode());
        }
}

输出结果如下:

三、反射能干什么

这可能是大家最关心的一部分,很多的文字可能一上来就讲这些东西,但是我希望前文和本文的前半部分能够帮大家更加清晰的理解反射相关的概念,在使用的时候带着这些概念就会有更深入的理解,不至于一头雾水。

3.1 获取类的基本信息

package com.lazy.snail.day67;

/**
 * @ClassName Day67Demo2
 * @Description TODO
 * @Author lazysnail
 * @Date 2026/3/6 11:32
 * @Version 1.0
 */
public class Day67Demo2 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> clazz = Class.forName("com.lazy.snail.day67.Student");

        System.out.println("全类名:" + clazz.getName());
        System.out.println("简单类名:" + clazz.getSimpleName());
        System.out.println("父类:" + clazz.getSuperclass().getName());
        System.out.println("修饰符:" + clazz.getModifiers());
    }
}

3.2 获取构造方法创建对象

package com.lazy.snail.day67;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

/**
 * @ClassName Day67Demo3
 * @Description TODO
 * @Author lazysnail
 * @Date 2026/3/6 11:43
 * @Version 1.0
 */
public class Day67Demo3 {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        Class<?> clazz = Student.class;

        // 获取私有有参构造
        Constructor<?> constructor = clazz.getDeclaredConstructor(String.class, int.class);

        // 私有构造器必须关闭访问检查才能使用
        constructor.setAccessible(true);

        // 创建对象
        Object student = constructor.newInstance("懒惰蜗牛", 28);
        System.out.println(student);
    }
}

示例代码中先拿到Student的Class对象,再通过getDeclaredConstructor方法获取私有构造方法。

最后通过newInstance方法创建对象。

newInstance() 就是通过Class对象,让JVM走了一遍和new一模一样的创建流程,只不过这个过程是运行时动态决定的。

你可能会问,既然我都已经能够拿到Class对象了,也就是可以获取类的所有元数据了,为什么还要使用setAccessible来关闭访问检查,又显得有点多此一举了。

这其实就是能够看到和能够动手的区别,setAccessible(true)相当于一个授权的动作。Java语言规范里private是私有的,默认不能访问,但是你又有明确的需求,想要突破这个防线,那你就得手动授权下。

这样既有安全底线(安全),想闯进去也留了口子可以突破(灵活)。

Java9+引入模块系统后,即使setAccessible (true),对非open的包也会抛InaccessibleObjectException。除非用--add-opens JVM参数或者module-info.java中用opens关键字进行规避。

3.3 反射调用方法

package com.lazy.snail.day67;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * @ClassName Day67Demo4
 * @Description TODO
 * @Author lazysnail
 * @Date 2026/3/6 13:14
 * @Version 1.0
 */
public class Day67Demo4 {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        Class<?> clazz = Student.class;
        // 先创建对象
        Constructor<?> constructor = clazz.getDeclaredConstructor(String.class, int.class);
        constructor.setAccessible(true);
        Object student = constructor.newInstance("懒惰蜗牛", 28);

        // 获取 sayHi() 方法
        Method sayHiMethod = clazz.getDeclaredMethod("sayHi");
        // 调用
        sayHiMethod.invoke(student);
    }
}

通过getDeclaredMethod获取到sayHi这个方法,使用invoke调用方法。

invoke方法主要分三步走,第一步JVM去方法区找到Student.sayHi()这个方法的字节码指令(就是一串机器能执行的命令)。第二步把student这个对象绑定到方法上,相当于告诉方法:你这次执行用的是 '懒惰蜗牛' 这个对象的数据。第三步JVM执行找到的字节码指令,把结果返回(如果有返回值的话)。

3.4 反射操作字段

package com.lazy.snail.day67;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;

/**
 * @ClassName Day67Demo5
 * @Description TODO
 * @Author lazysnail
 * @Date 2026/3/6 13:22
 * @Version 1.0
 */
public class Day67Demo5 {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException {
        Class<?> clazz = Student.class;
        Constructor<?> constructor = clazz.getDeclaredConstructor(String.class, int.class);
        constructor.setAccessible(true);
        Object student = constructor.newInstance("懒惰蜗牛", 28);

        // 获取private字段name
        Field nameField = clazz.getDeclaredField("name");

        // 关闭访问权限检查
        nameField.setAccessible(true);

        // 获取值
        Object name = nameField.get(student);
        System.out.println("name = " + name);

        // 修改值
        nameField.set(student, "蜗牛懒惰");
        System.out.println(student);
    }
}

字段信息当然也是类元数据的一部分,自然可以获取,修改。

从底层来看,nameField.set也可以分三步走。

JVM先根据nameField这个Field对象里记录的信息(字段名、字段类型、在对象中的偏移量),找到student对象里name字段具体在内存的哪个位置。

做一些必要的检查,比如检查我们要赋的值 "蜗牛懒惰" 是不是String类型(因为name字段是String)。类型不对就抛异常。

最后直接把新值的引用(或基本类型的值)写到找到的那个内存位置。

3.5 Method/Field的缓存

在反射的使用过程中,像getDeclaredMethod和getDeclaredField方法非常消耗性能,因为每次调用都会重新解析类的元数据,一般在实际的使用过程中,我们都会把这些对象缓存起来。

package com.lazy.snail.day67;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * @ClassName Day67Demo6
 * @Description TODO
 * @Author lazysnail
 * @Date 2026/3/6 13:31
 * @Version 1.0
 */
public class Day67Demo6 {

    private static final Method SAY_HI_METHOD;
    private static final Field NAME_FIELD;

    static {
        try {
            // 缓存方法
            SAY_HI_METHOD = Student.class.getDeclaredMethod("sayHi");
            SAY_HI_METHOD.setAccessible(true);

            // 缓存字段
            NAME_FIELD = Student.class.getDeclaredField("name");
            NAME_FIELD.setAccessible(true);
        } catch (NoSuchMethodException | NoSuchFieldException e) {
            throw new RuntimeException("初始化反射缓存失败", e);
        }
    }

    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        Class<?> clazz = Student.class;
        Constructor<?> constructor = clazz.getDeclaredConstructor(String.class, int.class);
        constructor.setAccessible(true);
        Object student = constructor.newInstance("懒惰蜗牛", 28);

        // 直接用缓存的方法调用
        SAY_HI_METHOD.invoke(student);
        // 直接用缓存的字段修改
        NAME_FIELD.set(student, "蜗牛");
        System.out.println(student);
    }
}

其实很简单,就是把获取过程放到静态代码块里,然后把获取到的信息存下来,使用的时候就不需要再去重新解析类元数据了。

四、反射很慢?

很多人一提到反射,脑子里蹦出来的一个想法就是这东西慢。至于哪里慢,怎么优化,其实很少人去说。

根据一些基准测试数据和HotSpot的具体实现,反射慢主要体现在这几个方面。

首先, 反射会涉及到一些额外的安全、类型检查,每次invoke都会重新校验访问权限、参数类型匹配,这是最大开销。

你说为啥要搞这些额外的检查,你想想,咱们正常代码在编译期就过了安检,我们通过反射修修改改,JVM总要确保我们修改的东西是安全的才能放行,没再次安检,把JVM搞崩了算谁的?

再者对参数的拆装箱和异常包装,invoke接收Object []参数,基本类型会自动装箱,异常会被包装为 InvocationTargetException。

其次就是JIT优化的限制,反射调用会被JIT当成黑盒,没办法像直接调用那样做内联、逃逸分析等深度优化。

这其实比较好理解,JIT是把热点代码编译成本地机器码直接运行,不用每次都解释执行。

但是反射的“动态”本质就是不确定,不确定调哪个方法、不确定参数类型、不确定访问权限。所以JIT把反射当成黑盒。

再说说HotSpot,从Java18+就把反射底层重构成了MethodHandle,部分invoke已经用MethodHandle实现了,MethodHandle在HotSpot中性能提升的本质其实是让上面我们提到的JIT眼中的黑盒变成了灰盒。让JIT能够看透调用关系,把动态调用优化得接近静态调用。

所以说相对于直接调用,反射本身确实慢,就算用上缓存也比不上直接调用。

但真正让反射慢到不能用的,是高频滥用又不缓存。

结语

再回过头看,反射其实没有那么神秘。

也不过就是Java在运行时给我们留的一扇后门,当我们拿着一个对象又不知道他是怎么来的,当我们需要调用一个在编译期还不存在的方法的时候,当想突破封装写更通用的框架时候,我们就能用上。

我们在业务代码的编写过程中,其实使用反射的频率不高。

因为业务代码就是需要简单直接、可读性强,如果在业务代码中频繁使用这把牛刀,反而会让简单的问题复杂化。

下一篇预告

待定

如果你觉得这系列文章对你有帮助,欢迎关注专栏,我们一起坚持下去!

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐