选择语言
durumis AI 总结的文章
- 反射是在執行時存取類別資訊並操作類別的 API。
- 透過反射,可以建立類別實例,並不受存取修飾詞限制地存取欄位和方法,尤其在大型開發階段中,框架需要動態管理依賴關係時,它非常有用。
- 然而,它可能會破壞封裝性並降低效能,因此最好只在必要時使用。
反射是什么?
反射是通过加载到堆内存中的Class类型对象,支持创建所需类的实例,并支持访问实例的字段和方法,无论访问控制符如何。
这里提到的加载类是指,JVM的类加载器完成对类文件加载后,创建一个包含该类信息的Class类型对象并将其存储在内存的堆中。请注意,这与使用new关键字创建的对象不同。如果对Class类型对象不了解,建议参考java.lang.class对象的JDK文档。
使用方法
在使用反射之前,需要获取加载到堆内存中的Class类型对象。共有三种方法。
- 通过类.class获取
- 通过实例.getClass()获取
- 通过Class.forName("类名")获取
public class Member {
private String name;
protected int age;
public String hobby;
public Member() {
}
public Member(String name, int age, String hobby) {
this.name = name;
this.age = age;
this.hobby = hobby;
}
public void speak(String message) {
System.out.println(message);
}
private void secret() {
System.out.println("密码是1234。");
}
@Override
public String toString() {
return "Member{" +
"name='" + name + '\'' +
", age=" + age +
", hobby='" + hobby + '\'' +
'}';
}
}
public class Main {
public static void main(String[] args) throws ClassNotFoundException {
Class memberClass = Member.class;
System.out.println(System.identityHashCode(memberClass));
Member member = new Member("Jayon", 23, "Da Rats 开发");
Class extends Member> memberClass2 = member.getClass();
System.out.println(System.identityHashCode(memberClass2));
Class> memberClass3 = Class.forName("{包名}.Member");
System.out.println(System.identityHashCode(memberClass3));
}
}
// 执行结果
1740000325
1740000325
可以确认这三种方式获取的Class类型实例都是相同的。无论使用哪种方式,哈希值都相同,因此可以根据情况选择合适的方式使用。
现在,我们可以通过获取的Class类型来创建该类的实例,并访问实例的字段和方法,不受访问控制符的限制。首先,尝试创建该类的实例。
public class Main {
public static void main(String[] args) throws Exception {
// 打印Member的所有构造函数
Member member = new Member();
Class extends Member> memberClass = member.getClass();
Arrays.stream(memberClass.getConstructors()).forEach(System.out::println);
// 使用Member的默认构造函数创建实例
Constructor extends Member> constructor = memberClass.getConstructor();
Member member2 = constructor.newInstance();
System.out.println("member2 = " + member2);
// 使用Member的其他构造函数创建实例
Constructor extends Member> fullConstructor =
memberClass.getConstructor(String.class, int.class, String.class);
Member member3 = fullConstructor.newInstance("Jayon", 23, "Da Rats 开发");
System.out.println("member3 = " + member3);
}
}
// 执行结果
public Member()
public Member(java.lang.String,int,java.lang.String)
member2 = Member{name='null', age=0, hobby='null'}
通过getConstructor()获取构造函数,并使用newInstance()动态创建Member实例。
最后,让我们尝试访问实例的字段和方法,无论访问控制符如何。
public class Main {
public static void main(String[] args) throws Exception {
Member member = new Member("Jayon", 23, "Da Rats 开发");
Class extends Member> memberClass = member.getClass();
// 访问字段
Field[] fields = memberClass.getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
System.out.println(field.get(member));
}
fields[0].set(member, "Jayon2");
System.out.println(member);
// 访问方法
Method speakMethod = memberClass.getDeclaredMethod("speak", String.class);
speakMethod.invoke(member, "反射测试");
Method secretMethod = memberClass.getDeclaredMethod("secret");
secretMethod.setAccessible(true);
secretMethod.invoke(member);
}
}
// 执行结果
Jayon
23
Da Rats 开发
Member{name='Jayon2', age=23, hobby='Da Rats 开发'}
反射测试
通过getDeclaredFileds()获取类实例变量,通过get()获取字段值,通过set()修改字段值。需要注意的是,访问private访问控制符的字段时,需要将setAccessible()的参数设置为true。
同样地,可以使用getDeclaredMethod()获取方法。此时需要将方法名和参数类型作为参数传递。同样地,访问private访问控制符的方法时,需要将setAccessible()的参数设置为true。最后,使用invoke()方法调用通过反射API获取的方法。
优缺点
- 优点
- 在运行时,可以创建类的实例,访问字段和方法,不受访问控制符的限制,从而提供灵活性。
- 缺点
- 破坏封装性。
- 在运行时创建实例,无法在编译时进行类型检查。
- 在运行时创建实例,难以了解具体的运行流程。
- 与直接访问字段和方法相比,使用反射访问的性能较低(并非所有情况下性能都较低)。
使用原因
通过反射API,可以在运行时访问类信息,并根据需要操作类。即使是使用private访问控制符声明的字段和方法,也可以进行操作。这似乎是一种违反面向对象设计中封装性原则的技术,不应该使用。
在小型控制台程序中,开发人员可以在编译时了解程序中使用的所有对象及其依赖关系。但是,在大型框架开发中,很难了解大量对象及其依赖关系。此时,可以使用反射动态创建类,并建立依赖关系。
例如,在Spring的Bean工厂中,只需添加@Controller、@Service、@Repository等注解,Bean工厂就能识别出带有这些注解的类,并创建和管理这些类。虽然开发人员没有显式地将这些类告知Bean工厂,但这一切都得益于反射。在运行时,会搜索并识别带有这些注解的类,然后使用反射创建这些类的实例,注入所需的字段,并将其存储在Bean工厂中。
当然,如上所述,反射会破坏封装性,因此建议仅在必要时使用。