反射机制让Java程序在运行期间也能查看类的内部结构,这在框架开发、注解处理和对象序列化等场景中非常常见。要获取一个类的属性和方法,第一步不是直接调用某个API,而是先拿到代表该类的Class对象。Class对象是JVM为每个已加载类维护的类型信息入口,后续对字段、构造器、方法的查询都从这里发起。如果拿不到Class对象,反射的一切操作都无从谈起。

下面从Class对象的获取开始,逐步拆解获取属性与方法的具体步骤,并给出可以运行的代码示例。
一、先拿到Class对象:反射操作的入口
Class对象的获取有三种常用方式。第一种是使用类字面量,例如Person.class。这种方式在编译期就确定了类型,不会抛出受检异常,执行效率较高,适合已经明确知道类名的情况。第二种是通过对象的getClass方法,任何Object都有这个方法,它返回对象实际运行时类的Class实例。第三种是使用Class.forName静态方法,传入类的全限定名,例如Class.forName("com.learning.reflect.Person")。这种方式最灵活,类名可以由配置文件或用户输入决定,但需要处理ClassNotFoundException。
三种方式各有适用场景。类字面量适合写死的工具类;getClass适合在方法内部根据传入对象动态判断;Class.forName适合加载驱动、解析XML配置等场景。下面的代码展示了三种获取方式:
public class ClassDemo {
public static void main(String[] args) throws Exception {
// 方式一:类字面量
Class clazz1 = Person.class;
// 方式二:对象的 getClass 方法
Person person = new Person();
Class clazz2 = person.getClass();
// 方式三:Class.forName 加载全限定名
Class clazz3 = Class.forName("com.learning.reflect.Person");
System.out.println(clazz1 == clazz2); // true
System.out.println(clazz2 == clazz3); // true
}
}
同一个类无论通过哪种方式获取,在同一个类加载器下返回的Class对象都是同一个实例,因此上面的比较结果为true。这也说明Class对象不是轻量级的数据副本,而是JVM内部真实类型信息的映射。理解这一点有助于后续操作,因为对任意一个Class对象获取到的字段和方法都代表同一份元数据。
二、获取类的属性:Field对象的遍历与访问
拿到Class对象以后,获取属性主要通过getDeclaredFields和getFields两个方法。getDeclaredFields返回当前类中声明的所有字段,不区分public、protected、private,但不包含继承自父类的字段。getFields则只返回public字段,并且包含从父类继承来的public字段。这个差异很容易造成排查困难:如果只想查看本类自己的字段,应该优先使用getDeclaredFields;如果需要对外暴露的公共成员,可以使用getFields。
下面的代码定义了一个带有私有字段的Person类,然后通过反射遍历它的所有字段:
import java.lang.reflect.Field;
public class FieldDemo {
public static void main(String[] args) {
Class clazz = Person.class;
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
System.out.println("字段名:" + field.getName());
System.out.println("字段类型:" + field.getType().getName());
System.out.println("修饰符:" + field.getModifiers());
}
}
}
class Person {
private String name;
private int age;
public String address;
}
从输出中可以看到,私有字段name和age都会被列出。Field类提供了多个实用方法:getName返回字段名称,getType返回字段的Class对象,getModifiers返回修饰符的整数表示,可以配合Modifier.toString方法转换为可读文本。如果需要读取或修改字段值,还可以使用get和set方法。但私有字段默认不允许直接访问,必须调用field.setAccessible(true)关闭访问检查,否则会抛出IllegalAccessException。
下面展示如何修改一个私有字段的值:
import java.lang.reflect.Field;
public class ModifyFieldDemo {
public static void main(String[] args) throws Exception {
Person person = new Person();
Class clazz = person.getClass();
Field nameField = clazz.getDeclaredField("name");
nameField.setAccessible(true);
nameField.set(person, "张三");
Field ageField = clazz.getDeclaredField("age");
ageField.setAccessible(true);
ageField.setInt(person, 25);
System.out.println(person.getName()); // 输出:张三
System.out.println(person.getAge()); // 输出:25
}
}
这里使用getDeclaredField按名称查找单个字段,比遍历数组更直接。setAccessible(true)会绕过Java访问控制,这在很多框架中用于注入依赖或反序列化数据,但也意味着封装性被打破,使用时需要谨慎。对于基本类型字段,Field提供了setInt、setBoolean等类型安全的重载方法,也可以统一使用set方法并传入包装类型。
三、获取类的方法:Method对象的查找与调用
获取方法与获取字段的思路类似,但方法更复杂,因为方法包含名称、参数列表、返回类型和异常声明。Class类提供了getDeclaredMethods和getMethods两个方法。getDeclaredMethods返回当前类声明的所有方法,包括私有方法,但不包含继承的方法;getMethods返回所有public方法,包括从父类继承的方法,但不包含私有方法。开发中如果只关心本类自己定义的方法,使用getDeclaredMethods即可。
遍历方法时,Method对象可以输出方法名、参数类型和返回类型。下面的示例展示如何打印一个类的全部方法签名:
import java.lang.reflect.Method;
public class MethodListDemo {
public static void main(String[] args) {
Class clazz = Person.class;
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
StringBuilder builder = new StringBuilder();
builder.append(method.getReturnType().getSimpleName());
builder.append(" ");
builder.append(method.getName());
builder.append("(");
Class[] params = method.getParameterTypes();
for (int i = 0; i < params.length; i++) {
if (i > 0) {
builder.append(", ");
}
builder.append(params[i].getSimpleName());
}
builder.append(")");
System.out.println(builder.toString());
}
}
}
代码中使用了StringBuilder拼接方法签名,注意循环中对于参数分隔符的处理。getParameterTypes返回参数类型的Class数组,getReturnType返回返回类型。如果要获取指定名称和参数列表的方法,可以使用getDeclaredMethod或getMethod,并传入参数类型的Class数组。例如Person类中有一个私有方法sayHello,可以这样获取并调用:
import java.lang.reflect.Method;
public class InvokeMethodDemo {
public static void main(String[] args) throws Exception {
Person person = new Person();
Class clazz = person.getClass();
Method method = clazz.getDeclaredMethod("sayHello");
method.setAccessible(true);
method.invoke(person);
}
}
invoke方法第一个参数是要调用方法的对象,如果是静态方法则可以传null。如果方法有参数,invoke后面按顺序传入对应参数值。私有方法同样需要setAccessible(true)才能调用。与字段访问一样,这种方式绕过了访问控制,适合测试或框架场景,但不应在业务代码中随意使用。
四、完整示例:同时获取属性和方法并调用
为了让流程更完整,下面给出一个综合示例。先定义一个包含私有字段、构造器、普通方法和私有方法的User类,然后通过反射获取字段并修改,再获取私有方法并调用。整个过程覆盖了反射获取属性和方法的核心步骤。
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class ReflectFullDemo {
public static void main(String[] args) throws Exception {
Class clazz = User.class;
// 创建对象
Object user = clazz.getDeclaredConstructor().newInstance();
// 获取并修改私有字段
Field usernameField = clazz.getDeclaredField("username");
usernameField.setAccessible(true);
usernameField.set(user, "李四");
Field ageField = clazz.getDeclaredField("age");
ageField.setAccessible(true);
ageField.setInt(user, 30);
// 获取并调用私有方法
Method showMethod = clazz.getDeclaredMethod("showInfo");
showMethod.setAccessible(true);
showMethod.invoke(user);
}
}
class User {
private String username;
private int age;
public User() {}
private void showInfo() {
System.out.println("用户名:" + username + ",年龄:" + age);
}
}
运行这个示例会输出“用户名:李四,年龄:30”。这里使用了getDeclaredConstructor().newInstance()来创建对象,这是反射创建实例的常用方式。对于私有字段和方法,都调用了setAccessible(true)。整个过程可以概括为:获取Class对象、获取Field或Method对象、关闭访问检查、执行读取或调用操作。每个步骤都对应一个明确的反射API,掌握这个路径后,处理更复杂的反射需求也会更加顺手。
反射虽然灵活,但它绕过了编译期类型检查,代码的可读性和性能都会受到一定影响。在实际业务中,反射更适合框架底层、工具类或需要动态加载类的场景。如果只是普通业务代码,建议优先使用直接的字段访问和方法调用,只有在确实需要运行时动态处理时才引入反射。

