Java反射基础

目录


前言

通过本篇文章,你将了解到 Java 反射的基本概念、为什么需要反射,以及如何获取 Class 对象并进行基础操作。


1. 字节码与类加载机制

在学习反射之前,我们先来简单了解一下字节码文件与 JVM 中的类加载机制。

字节码

我们编写的 Java 源代码保存在 .java 文件中。当我们在 IDE 中编译或运行代码时,编译器会将这些源文件编译成 JVM 可执行的字节码文件,其后缀为 .class

类加载机制

从编写代码到程序运行,主要经历两个时期:编译期运行期

  • 编译期 :程序员编写的 .java 代码被编译成 .class 字节码文件的阶段。
  • 运行期 :JVM 找到并加载 .class 文件,将其数据读入内存中,并在堆区生成一个对应的 Class 对象。这个阶段被称为类加载

这个 Class 对象存储了该类的结构信息(如类名、父类、实现的接口、构造方法、属性和普通方法等)。

反射 的核心,就是通过获取这个 Class 对象,来读取和操作该类的信息。


2. 获取 Class 对象的三种方式

理解了反射需要依赖 Class 对象后,我们来看一下在 Java 中获取 Class 对象的三种常用方式:

方式一:Class.forName("全类名")

全类名的意思是:package名+这个类的名字

假设有如下类:

java 复制代码
package com.example;

public class User {
    // 省略属性与方法...
}

那么该类的全类名为 "com.example.User"

获取其 Class 对象的完整写法:

java 复制代码
Class<?> clazz = Class.forName("com.example.User");

关于 Class<?>:其含义是某种具体的类型的class对象。

  • ? 是通配符,表示某种未知的具体类型。

  • 通过名字去查找,编译器无法确定该类具体是什么类型,所以使用 Class<?>

  • 可以通过强制类型转换实现:

    java 复制代码
    Class<User> clazz = (Class<User>) Class.forName("com.example.User");
  • 由于是通过字符串名称查找,找不到的话,会抛出已检查异常 ClassNotFoundException

方式二:类名.class

如果我们已经在写代码的时候,明确知道要操作哪个类。

java 复制代码
Class<User> clazz = User.class;

在编译期进行类型检查,如果类名写错,编译器会直接报错,不需要处理 ClassNotFoundException 异常。

方式三:对象.getClass()

已经拥有了某个类的对象实例时,通过该实例获取其对应的 Class 对象:

java 复制代码
User bean = new User();
Class<? extends User> clazz = bean.getClass();

如果对象实例为 null,会抛出空指针异常 NullPointerException


3. 获取 Class 信息

获取到 Class 对象后,可以获取类的构造方法、字段以及普通方法。

获取构造方法

获取指定的被 public 修饰的构造方法。

clazz.getConstructor(Class<?>... parameterTypes)

获取指定的任意修饰符(包括 private)的构造方法。 clazz.getDeclaredConstructor(Class<?>... parameterTypes)

假设 User 类:

java 复制代码
public class User {
    private String name;
    private Integer age;

    // 构造方法一:无参构造
    public User() {}

    // 构造方法二:单参构造(String)
    public User(String name) {
        this.name = name;
    }

    // 构造方法三:多参构造
    public User(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    // 构造方法四:私有单参构造(int)
    private User(int age) {
        this.age = age;
    }
}

获取对应的构造器:

java 复制代码
Class<User> clazz = User.class;

// 获取无参构造方法
Constructor<User> c1 = clazz.getDeclaredConstructor();

// 获取单参构造方法(String)
Constructor<User> c2 = clazz.getDeclaredConstructor(String.class);

// 获取多参构造方法(String, Integer)
Constructor<User> c3 = clazz.getDeclaredConstructor(String.class, Integer.class);

// 获取私有构造方法(int)
Constructor<User> c4 = clazz.getDeclaredConstructor(int.class);

利用获取到的构造方法创建对象

java 复制代码
c4.setAccessible(true);
User bean = c4.newInstance(18);

private构造方法、字段或方法,在操作前必须调用 setAccessible(true)

默认情况下,访问权限检查为 false,强行访问私有成员会抛出 IllegalAccessException

获取字段

获取指定的单个属性(无论何种修饰符)

clazz.getDeclaredField(String name)

获取类中声明的所有属性。

clazz.getDeclaredFields()

java 复制代码
// 获取名为 "name" 的私有字段
Field field = clazz.getDeclaredField("name");

// 解除私有访问限制
field.setAccessible(true);

// 为指定对象的该属性赋值
field.set(bean, "张三");

// 获取指定对象的该属性值
String name = (String) field.get(bean);

关于 field.set(bean, "张三");

写bean的原因是,写明给哪一个实例化的对象的字段赋值

获取方法

获取指定的成员方法。需要传入方法名, 参数类型的 Class

clazz.getDeclaredMethod(String name, Class<?>... parameterTypes)

调用该方法。传入要执行的对象实例和具体的参数值。

method.invoke(Object obj, Object... args)

java 复制代码
// 获取 setName(String) 方法
Method method = clazz.getDeclaredMethod("setName", String.class);

// 执行方法
method.invoke(bean, "李四");

// 如果是静态方法,调用时对象实例传 null 即可
// staticMethod.invoke(null, args);

总结

  1. 反射的核心是 Class 对象 :它是 .class 字节码文件被 JVM 加载到内存后生成的类型元数据表示。
  2. 获取 Class 对象的三种方式
    • Class.forName("全类名") 适用于动态配置文件加载;
    • 类名.class 最为安全,适用于编译期已确定类型的场景;
    • 对象.getClass() 适用于已知对象实例的场景。
  3. 反射可打破封装限制 :通过调用 setAccessible(true),反射可以访问和操作包括 private 在内的私有构造方法、字段和方法。
  4. 反射是框架设计的基石:虽然反射增加了运行时的灵活性(如 Spring IoC、MyBatis 映射),但过度使用反射会带来性能开销,并且破坏了面向对象的封装性。