今天咱们把反射这个 Java 里的 "灵活刺客" 盘明白了 —— 它不像基础语法那样按部就班,而是能 "绕后突破",直接操控类的底层成员,就像王者里的兰陵王,能隐身摸到后排,出其不意。
大家好,我是程序员强子。
今天我们来练习一下底层操控力极高的【英雄】:反射
-
反射的 【基础技能包】
-
Class类对象的获取以及其用法
-
Constructor类及其用法
-
Field类及其用法
-
Method类及其用法
-
反射的【连招顺序】
-
先通过 Class 对象 【锁定目标】
-
再用 Method/Field/Constructor类 【等大招,先做准备,走位、摸眼或者闪现进场,准备开打】
-
最后method.invoke()【释放技能,combo连招组合拳】
发车啦~ 来不及解释了~
Class类
Class 类对象的获取
-
通过对象的getClass()方法(需已有实例),
-
适用于已知对象实例的场景,调用对象的getClass()方法返回其对应类的Class对象。
-
通过类名.class(无需实例)
-
适用于已知类名的场景,直接通过类名.class获取
-
通过Class.forName(String className)(运行时加载,常用)
-
适用于类名通过字符串动态指定的场景(如配置文件加载类),需要处理ClassNotFoundException。
示例:
public static void main(String[] args) {
String str = "hello";
Class<?> cls = str.getClass(); // 获取String类的Class对象
log.info("cls:{}", cls); // 输出:cls:class java.lang.String
Class<?> cls2 = String.class; // 获取String类的Class对象
Class<?> intCls = int.class; // 基本类型也可通过此方式
log.info("cls2:{},intCls:{}",cls2,intCls); // 输出:cls2:class java.lang.String,intCls:int
try {
Class<?> cls3 = Class.forName("java.lang.String"); // 全限定名
log.info("cls3:{}",cls3); // 输出:class java.lang.String
} catch (ClassNotFoundException e) {
log.error("ReflectionMain error",e);
}
}
getName()、getCanonicalName()与getSimpleName()的区别
-
getName()
-
返回类的全限定名(包含包名),数组 / 内部类有特殊格式(JVM 规范格式)
-
普通类:com.example.Test;
-
数组Test[]:[Lcom.example.Test;
-
getCanonicalName()
-
返回类的规范全限定名(更易读),数组格式为类型[]
-
普通类:com.example.Test;
-
数组Test[]:com.example.Test[]
-
getSimpleName()
-
返回类的简单名称(不含包名),内部类仅显示内部类名
-
普通类:Test;
-
数组Test[]:Test[]
getFields()和getDeclaredFields()的区别
-
getFields()
-
获取当前类及所有父类中被 public 修饰的字段(公有字段)
-
例子:如果User类有 public 字段name,其父类Person有 public 字段age,那么User.class.getFields()会同时返回name和age
-
getDeclaredFields()
-
获取当前类自己声明的所有字段(不管权限,public、private、protected、默认权限都算),但不包括父类的任何字段(哪怕父类的字段是 public)
-
例子:User类有 public 字段name和 private 字段password,其父类Person有 public 字段age,那么User.class.getDeclaredFields()只会返回name和password,不会返回age。
一句话总结区别:
- getFields() = 公有字段(自己的 + 父类的);
- getDeclaredFields() = 自己的所有字段(不限权限,不含父类的)
Constructor类
Constructor类代表类的构造方法,通过Class对象的方法获取,可用于动态创建对象。
核心方法
- getConstructor(Class<?>... parameterTypes):获取public构造方法(参数类型匹配)。
- getDeclaredConstructor(Class<?>... parameterTypes):获取类中所有访问修饰符的构造方法(包括 private)。
- newInstance(Object... initargs):通过构造方法创建实例(参数为构造方法的入参)。
- setAccessible(boolean flag):设置是否忽略访问权限检查(用于访问 private 构造方法)。
示例
定义一个Person对象,该对象有两个私有字段,并且有两个构造方法,其中一个是私有的无参构造,另外一个是public的有参构造
@Data
public class Person {
private String name;
private int age;
// 无参构造(private)
private Person() {}
// 有参构造(public)
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + "}";
}
}
获取public有参构造并创建实例 && 获取private无参构造并创建实例(需忽略访问权限)
public static void main(String[] args) throws Exception {
Class<?> personCls = Person.class;
// 1. 获取public有参构造并创建实例
Constructor<?> publicConstructor = personCls.getConstructor(String.class, int.class);
Person p1 = (Person) publicConstructor.newInstance("DonaldCen", 18);
System.out.println(p1); // 输出:Person{name='DonaldCen', age=18}
// 2. 获取private无参构造并创建实例(需忽略访问权限)
Constructor<?> privateConstructor = personCls.getDeclaredConstructor();
privateConstructor.setAccessible(true); // 允许访问private
Person p2 = (Person) privateConstructor.newInstance();
System.out.println(p2); // 输出:Person{name='null', age=0}
}
Field类
Field类代表类的成员变量(字段),通过Class对象的方法获取,可用于动态读写字段值。
核心方法
- getField(String name):获取public字段(包括父类的 public 字段)。
- getDeclaredField(String name):获取类中所有访问修饰符的对应name的字段(仅本类,不含父类)。
- getDeclaredFields():获取所表示的类或接口的所有(包含private修饰的)字段,不包括继承的字段
- set(Object obj, Object value):为对象obj的当前字段设置值(静态字段obj可为 null)。
- get(Object obj):获取对象obj的当前字段值(静态字段obj可为 null)。
- setAccessible(boolean flag):忽略访问权限检查(用于访问 private 字段)。
示例
还是以Person类为例子,修改其中一个字段name为public
@Data
public class Person {
public String name;
private int age;
// 有参构造(public)
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + "}";
}
操作public字段(name) && 操作private字段(age)
public static void main(String[] args) throws Exception {
Person p = new Person("DonaldCen", 18);
Class<?> cls = p.getClass();
// 1. 操作public字段(name)
Field nameField = cls.getField("name");
nameField.set(p, "DonaldCen666"); // 修改name值
System.out.println(nameField.get(p)); // 获取name值,输出:DonaldCen666
// 2. 操作private字段(age)
Field ageField = cls.getDeclaredField("age");
ageField.setAccessible(true); // 允许访问private
ageField.set(p, 30); // 修改age值
System.out.println(ageField.get(p)); // 获取age值,输出:30
}
Method类
Method类代表类的方法,通过Class对象的方法获取,可用于动态调用方法。
核心方法
- getMethod(String name, Class<?>... parameterTypes):获取public方法(包括父类的 public 方法)。
- getDeclaredMethod(String name, Class<?>... parameterTypes):获取类中所有访问修饰符的方法(仅本类,不含父类)。
- invoke(Object obj, Object... args):调用对象obj的当前方法(参数为方法入参,静态方法obj可为 null)。
- setAccessible(boolean flag):忽略访问权限检查(用于访问 private 方法)。
示例
还是以Person类为例子,增加了两个方法,一个公有的public方法,一个是private方法
@Data
public class Person {
public String name;
private int age;
// 无参构造(private)
private Person() {}
// 有参构造(public)
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public void sayHello() { // public方法
System.out.println("Hello!");
}
private String getInfo(String prefix) { // private方法
return prefix + ":我是一个有趣的人";
}
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + "}";
}
调用public方法(sayHello) && 调用private方法(getInfo)
public static void main(String[] args) throws Exception {
Person p = new Person("DonaldCen",18);
Class<?> cls = p.getClass();
// 1. 调用public方法(sayHello)
Method sayHelloMethod = cls.getMethod("sayHello");
sayHelloMethod.invoke(p); // 输出:Hello!
// 2. 调用private方法(getInfo)
Method getInfoMethod = cls.getDeclaredMethod("getInfo", String.class);
getInfoMethod.setAccessible(true); // 允许访问private
String result = (String) getInfoMethod.invoke(p, "提示");
System.out.println(result); // 输出:提示:我是一个有趣的人
}
反射机制原理
执行流程
-
先定目标:明确你要 “操作” 什么
-
先想清楚反射的目的:是要动态创建对象(实例化)?还是读写字段值(比如改个私有属性)?或是调用某个方法(哪怕是私有方法)?
-
就像王者里先确定 “这波要开龙” 还是 “抓边”,目标清晰了才好选技能。
-
起手式:获取 Class 对象,选对 “入口”
-
不管干啥,第一步都得拿到 Class 对象(反射的 “总控制台”),三种方式按需选:
-
已知实例:用对象.getClass()(比如user.getClass()),像 “从英雄本体查信息”;
-
已知类名(字符串):用Class.forName("全类名")(比如加载数据库驱动),像 “远程召唤英雄”;
-
已知具体类:用类名.class(比如User.class),像 “直接锁定英雄模板”。
-
选技能:按需求挑 API,分清 “权限” 和 “范围”
-
根据目标选对应的 API,核心是分清 “公有 / 私有”“自己的 / 父类的”:
-
实例化对象:用 Constructor 类。公有构造器用getConstructor(参数类型),私有构造器用getDeclaredConstructor(参数类型)(记得用setAccessible(true)破权限);
-
操作字段:用 Field 类。公有字段(含父类)用getField("字段名"),自己的所有字段(含私有,不含父类)用getDeclaredField("字段名");
-
调用方法:用 Method 类。公有方法(含父类)用getMethod("方法名", 参数类型),自己的所有方法(含私有,不含父类)用getDeclaredMethod("方法名", 参数类型)。
-
放技能:执行操作,完成目标最后一步直接执行:
-
实例化:Constructor 调用newInstance(参数值);
-
改字段:Field 调用set(对象, 新值),读字段用get(对象);
-
调方法:Method 调用invoke(对象, 参数值)。
(注意:操作私有成员时,必须先setAccessible(true)解除权限限制,否则会 “技能空放” 报错。)
反射的线程安全设计
Java 反射 API 在设计时严格保证了线程安全性,主要依赖两方面机制:
-
缓存的线程安全访问
-
Class 类中存储反射元信息的reflectionData(包含方法、字段、构造器等缓存)通过volatile修饰 + CAS 操作保证可见性和原子性。
-
不可变的元信息载体
-
反射获取的Method、Field、Constructor对象本身是不可变的(其内部存储的方法名、参数类型等元信息一旦创建就无法修改)
-
即使多个线程共享这些对象,也不会出现线程安全问题(唯一可修改的accessible标志通过volatile修饰,保证读写可见性)
确保多线程环境下反射操作的稳定性(如框架中并发反射调用 Bean 方法时不会出现异常)
软引用(SoftReference)缓存 class 元信息
Class 类内部通过
private transient SoftReference reflectionData
缓存反射元信息(ReflectionData是一个内部类,包含methods、fields、constructors等数组)
- 软引用的特性是 “内存充足时保留,内存不足时被 JVM 回收”。反射元信息(如方法列表)属于高频访问但非核心数据,用软引用缓存可在减少重复解析元信息开销的同时,避免内存泄漏(若用强引用,即使类已无用,缓存仍可能占用内存)
- 当软引用被回收(reflectionData.get() == null)时,反射方法会重新从 JVM 的方法区(元空间)解析类元信息,重建ReflectionData并重新缓存
大幅降低反射操作的性能开销(解析类元信息是耗时操作),同时适应 JVM 的内存管理策略
方法 / 字段 / 构造器的拷贝机制(数据隔离)
当通过getMethod()、getField()等方法获取反射对象时,JVM 返回的是元信息的拷贝实例,而非内部缓存的原始数组元素。
- 避免反射操作中的交叉污染,保证多线程 / 多场景下反射对象的独立性
今天超神!