Skip to content

反射

1. RTTI 与反射

  • RTTI(Run-Time Type Identification):运行时类型识别,用于在程序运行时确定对象的类型。
  • 反射:将 Java 类中的各种成分(成员变量、方法、构造器、包等)映射成对应的 Java 对象,通过这些对象可以动态查看或操作类的内部结构。
  • 两种获取对象类型信息的方式:
    • “传统的”RTTI:编译时已知所有类型。
    • 反射机制:运行时动态发现和使用类的信息。

2. Class 类

  • Class 类位于 java.lang 包中,每个 Java 类都会在 JVM 中有一个唯一的 Class 对象,存储该类的类型信息。
  • 获取 Class 对象的主要方式
    • 直接通过类:类名.class
    • 通过对象:对象.getClass()
    • 通过全限定类名:Class.forName("包名.类名")
  • 常用方法
    • getName():返回全限定类名(包括包名)。
    • getSimpleName():返回类名,不含包名。
    • newInstance():调用无参构造器实例化对象(要求类必须有无参构造器)。

3. 类加载机制

  • 类加载流程:源代码先被编译成字节码,再由类加载子系统加载到 JVM 中,生成对应的 Class 对象。
  • 双亲委托机制:确保加载过程有序且安全,避免重复加载。

4. 反射核心组件与使用

4.1 Constructor 类(构造方法)

  • 作用:表示类的构造方法,可以在运行时动态创建对象。
  • 常用方法
    • getConstructor(…) / getDeclaredConstructor(…):获取构造器对象(区别在于是否包含私有构造器)。
    • newInstance(…):用构造器创建新实例。

4.2 Field 类(成员变量)

  • 作用:表示类的字段(包括静态字段与实例字段)。
  • 获取方式
    • getField(…):获取公共字段(包含父类)。
    • getDeclaredField(…):获取当前类中声明的字段(包括 private、protected)。
  • 常用操作
    • set(Object obj, Object value):动态设置字段的值。
    • get(Object obj):获取字段的值。
    • 注意:对于 private 字段,需调用 setAccessible(true) 以绕过访问限制。

4.3 Method 类(方法)

  • 作用:表示类中的方法,可通过反射调用方法。
  • 获取方式
    • getMethod(…):获取公共方法(包括继承方法)。
    • getDeclaredMethod(…):获取当前类中声明的方法(包括私有方法)。
  • 调用方法
    • invoke(Object obj, Object... args):在指定对象上调用该方法。
    • 若调用私有方法,同样需要 setAccessible(true)

5. 反射操作流程

  1. 获取 Class 对象:可以通过上述三种方式中的任意一种。
  2. 获取反射组件
    • 构造器、字段、方法均通过 Class 对象提供的 API 获取。
  3. 动态操作
    • 通过 Constructor.newInstance() 创建对象。
    • 通过 Field.set() / Field.get() 修改或读取字段值。
    • 通过 Method.invoke() 调用方法。

6. 注意事项与扩展

  • 访问控制:反射可以访问私有成员,但需调用 setAccessible(true);对于 final 修饰的字段,即使反射修改,其实际值也不会改变。
  • 性能问题:反射相比直接调用具有较低的运行时性能,需在合理的场景中使用,如动态代理、依赖注入和插件框架。
  • 类加载器:Class 对象由 JVM 通过类加载器加载,了解加载顺序和双亲委托机制对理解反射原理有帮助。

7. 从.class文件到Class对象

  • .class文件保存了类的所有信息,这些信息底层以0101(二进制)存储。
  • 通过不同的解读规则,.class文件可以表现为人类可读的源码或机器可执行的字节码。
  • **类加载器(ClassLoader)负责将.class文件加载到内存中,并生成对应的Class对象。
  • 加载过程大致分为以下三个步骤:
    1. 检查是否已加载:首先检查当前虚拟机中是否已存在该类,避免重复加载。
    2. 父优先规则(双亲委派):如果缓存中没有,则按照父加载器优先的原则尝试加载。
    3. 调用findClass():如果以上步骤都未加载成功,调用子类重写的findClass()方法,通过IO读取字节数据,再调用defineClass()将字节数组转换为Class对象。
  • 经典的loadClass方法流程如下: ```java protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 检查是否已经加载 Class<?> c = findLoadedClass(name); if (c == null) { try { // 遵循双亲委派机制 if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // 父加载器未能加载,继续 }
            if (c == null) {
                // 调用findClass()进行加载
                c = findClass(name);
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
    

    } - 其中,子类必须重写`findClass()`方法,定义读取.class文件(例如通过IO解析字节数组)的逻辑,并调用`defineClass()`生成Class对象:java @Override public Class<?> findClass(String name) throws ClassNotFoundException { try { byte[] datas = getClassData(name); if(datas == null) { throw new ClassNotFoundException("类没有找到:" + name); } return defineClass(name, datas, 0, datas.length); } catch (IOException e) { e.printStackTrace(); throw new ClassNotFoundException("类找不到:" + name); } } ```

8. Class对象的构建与内容

  • Class对象是对一个.class文件的完整抽象。JVM在加载.class文件后,会生成一个对应的Class对象,其中包含的信息有:

    • 字段、方法、构造器信息(为了详细描述这些信息,JDK定义了FieldMethodConstructor等类)
    • 注解泛型信息
    • Class对象的构造器是私有的,只有JVM可以创建,保证了同一个类在内存中只对应一个Class对象。
    • 通过Class.forName()等API可以获得Class对象。

    9. 实例化对象的过程

当在main方法中执行如下代码:

Person p = new Person();

实际经过以下步骤:

  1. 调用new操作符
    • new操作符会触发JVM为该对象在堆中分配内存,并初始化对象头(包括类型信息、锁信息等)。
  2. 获取Class对象
    • 无论是通过new操作还是反射,由于最终都依赖于Class对象,因此JVM首先要确定所引用的Class(可以通过编译时或运行时加载来获取Class对象)。
  3. 调用构造器
    • 对象的实例化本质上依赖于构造器。Class.newInstance()方法(或通过反射获取其他构造器的newInstance())在内部调用的是对应构造器对象。
    • 如果类没有无参构造器,调用newInstance()方法时会抛出异常,这也是为什么反射实例化对象需要保证有可用的构造函数的原因。

10. 反射API与对象调用

  • 反射API的两个主要目的:
    1. 动态创建实例:通过Class对象的newInstance()或者通过获取特定构造器后调用Constructor.newInstance()
    2. 反射调用方法:通过Method.invoke()来动态调用对象的方法。
  • 重点理解:实际上Class.newInstance()底层也是调用的构造器的newInstance()。因此,如果希望通过反射创建实例,编写类时需要提供无参构造器(或者必须显式使用有参构造器)。
  • 当对象调用方法时,JVM除了将方法共用外,还通过隐式传递当前调用对象参数的机制来保证方法操作的是正确的对象数据。
  • 在反射调用中,通过method.invoke(obj, args),第一个参数指定了目标对象,从而确定具体调用哪个对象的实例方法(静态方法则不需要传入目标对象)。
  • 类加载流程:从字节码文件(.class)到加载器将字节数据转成Class对象;Class对象中包含了所有类的详细信息(字段、方法、构造器、注解、泛型等)。
  • 实例化过程:JVM为对象在堆中分配内存,并使用构造器(通过反射API或new操作符)初始化对象。
  • 反射与构造器:反射机制通过获取到的Class对象内部的Constructor对象来创建实例,反射API在调用方法时,总是显式传入目标实例以解决数据调用的对应关系。

11. 示例代码


package com.example;

import java.lang.reflect.Method;

public class HelloReflect {
    public static void main(String[] args) throws Exception {
        // 1. 获取 Class 对象的三种方式
        Class<?> clazz1 = MyClass.class;
        Class<?> clazz2 = new MyClass().getClass();
        Class<?> clazz3 = Class.forName("com.example.MyClass");

        // 2. 实例化对象(要求类有无参构造器)
        MyClass instance = (MyClass) clazz3.newInstance();

        // 3. 获取方法并调用
        Method method = clazz3.getMethod("sayHello", String.class);
        method.invoke(instance, "反射调用");
    }
}
package com.example;

public class MyClass {
    public void sayHello(String name) {
        System.out.println("Hello, " + name);
    }
}