这是我参与「第四届青训营 」笔记创作活动的第6天。
反射(Reflection)是Java程序开发语言的特征之一,它允许运行中的Java程序对自身进行检查,或者说“自审”,并能直接操作程序的内部属性和方法。
什么是反射
反射的概念是由Smith在1982年首次提出的,主要是指程序可以访问、检测和修改它本身状态或行为的一种能力。通俗地讲,一提到反射,我们就可以想到镜子。镜子可以明明白白地照出我是谁,还可以照出别人是谁。反映到程序中,反射就是用来让开发者知道这个类中有什么成员,以及别的类中有什么成员。
反射为什么存在
反射好像破坏了java的封装性,就连私有变量都可以被外部所访问,使得类不是很安全。
然而官方的解释是:
- 反射让开发人员可以通过外部类的全路径名创建对象,并使用这些类,实现一些扩展的功能。
- 反射让开发人员可以枚举出类的全部成员,包括构造函数、属性、方法。以帮助开发者写出正确的代码。
- 测试时可以利用反射API访问类的私有成员,以保证测试代码覆盖率。
由此可知,反射是给程序员使用的工具。
反射API
Class对象
Class对象就是用来创建类的所有“常规“对象的,每一个类都有一个Class对象,每当编译了一个新类,就会产生一个Class对象,保存在同名的.class文件中。
获取 Class 对象的三种方式
新建一个项目,然后新建包com.test.reflection,在com.test.reflection包中新建一个Person类
package com.test.reflection;
public class Person {
private String PersonName;
public int PersonAge;
public Person() {
}
private Person(String PersonName) {
this.PersonName = PersonName;
}
public void setPersonAge(int PersonAge) {
this.PersonAge = PersonAge;
}
private String show(String message) {
System.out.println("show: " + PersonName + "," + PersonAge + "," + message);
return "testReturnValue";
}
}
- 获取Class对象
// 1.通过字符串获取Class对象,这个字符串必须带上完整路径名
Class PersonClass = Class.forName("com.test.reflection.Person");
// 2.通过类的class属性
Class PersonClass2 = Person.class;
// 3.通过对象的getClass()函数
Person PersonObject = new Person();
Class PersonClass3 = PersonObject.getClass();
- 第一种方法是通过类的全路径字符串获取 Class 对象,这也是最常用的反射获取 Class 对象的方法;
- 第二种方法通过类名获得,有限制条件:需要导入类的包;
- 第三种方法已经有了 Person 对象,不再需要反射。
通过这三种方式获取到的 Class 对象是同一个,也就是说 Java 运行时,每一个类只会生成一个 Class 对象。
- 获取属性
//公共属性
Field[] fields = perClazz.getFields() ;
for(Field field:fields) {
System.out.println(field);
}
System.out.println("====");
//所有属性 (属性的 :公共属性\所有属性的区别 同“方法”)
Field[] declaredFields = perClazz.getDeclaredFields() ;
for(Field declaredField:declaredFields) {
System.out.println(declaredField);
}
运行程序,输出如下:
declared Field: private java.lang.String com.test.reflection.Person.personName
declared Field: public int com.test.reflection.Person.personAge
field: public int com.test.reflection.Person.personAge
- 获取构造方法
// 1.获取所有声明的构造方法
Constructor[] declaredConstructorList = personClass.getDeclaredConstructors();
for (Constructor declaredConstructor : declaredConstructorList) {
System.out.println("declared Constructor: " + declaredConstructor);
}
// 2.获取所有公有的构造方法
Constructor[] constructorList = personClass.getConstructors();
for (Constructor constructor : constructorList) {
System.out.println("constructor: " + constructor);
}
运行程序,输出如下:
declared Constructor: public com.test.reflection.Person()
declared Constructor: private com.test.reflection.Person(java.lang.String)
constructor: public com.test.reflection.Person()\
- 获取普通方法
// 1.获取所有声明的函数
Method[] declaredMethodList = personClass.getDeclaredMethods();
for (Method declaredMethod : declaredMethodList) {
System.out.println("declared Method: " + declaredMethod);
}
// 2.获取所有公有的函数
Method[] methodList = personClass.getMethods();
for (Method method : methodList) {
System.out.println("method: " + method);
}
结果:
declared Method: public void com.test.reflection.Person.setPersonAge(int)
declared Method: private java.lang.String\ com.test.reflection.Person.show(java.lang.String)
method: public void com.test.reflection.Person.setPersonAge(int)
method: public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
method: public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
method: public final void java.lang.Object.wait() throws java.lang.InterruptedException
method: public boolean java.lang.Object.equals(java.lang.Object)
method: public java.lang.String java.lang.Object.toString()
method: public native int java.lang.Object.hashCode()
method: public final native java.lang.Class java.lang.Object.getClass()
method: public final native void java.lang.Object.notify()
method: public final native void java.lang.Object.notifyAll()\
从输出中我们看到,getMethods 方法不仅获取到了我们声明的公有方法setPersonAge,还获取到了很多 Object 类中的公有方法。这是因为我们前文已说到:Object 是所有 Java 类的父类。所有对象都默认实现了 Object 类的方法。 而getDeclaredMethods是无法获取到父类中的方法的。
应用
以 Person 类为例,如果此类在其他的包中,并且我们的需求是要在程序中通过反射获取他的构造方法,构造出 Person 对象,并且通过反射访问他的私有字段和私有方法
// 1.通过字符串获取Class对象,这个字符串必须带上完整路径名
Class personClass = Class.forName("com.test.reflection.Person");
// 2.获取声明的构造方法,传入所需参数的类名,如果有多个参数,用','连接即可
Constructor personConstructor = personClass.getDeclaredConstructor(String.class);
// 如果是私有的构造方法,需要调用下面这一行代码使其可使用,公有的构造方法则不需要下面这一行代码
personConstructor.setAccessible(true);
// 使用构造方法的newInstance方法创建对象,传入构造方法所需参数,如果有多个参数,用','连接即可
Object person = personConstructor.newInstance("NameA");
// 3.获取声明的字段,传入字段名
Field personAgeField = personClass.getDeclaredField("personAge");
// 如果是私有的字段,需要调用下面这一行代码使其可使用,公有的字段则不需要下面这一行代码
// personAgeField.setAccessible(true);
// 使用字段的set方法设置字段值,传入此对象以及参数值
personAgeField.set(person,10); //等同于 person.setpersonAge(10)
// 4.获取声明的函数,传入所需参数的类名,如果有多个参数,用','连接即可
Method personShowMethod = personClass.getDeclaredMethod("show",String.class);
// 如果是私有的函数,需要调用下面这一行代码使其可使用,公有的函数则不需要下面这一行代码
personShowMethod.setAccessible(true);
// 使用函数的invoke方法调用此函数,传入此对象以及函数所需参数,如果有多个参数,用','连接即可。函数会返回一个Object对象,使用强制类型转换转成实际类型即可
Object result = personShowMethod.invoke(person,"message");
System.out.println("result: " + result);
程序的逻辑注释已经写得很清晰了,我们再梳理一下:
- 先用第一种全路径获取 Class 的方法获取到了 Person 的 Class 对象
- 然后反射调用它的私有构造方法 private Person(String personName),构建出 newInstance
- 再将其公有字段 personAge 设置为 10
- 最后反射调用其私有方法 show,传入参数 “message”,并打印出这个方法的返回值。
其中,setAccessible 函数用于动态获取访问权限,Constructor、Field、Method 都提供了此方法,让我们得以访问类中的私有成员。
运行程序,输出如下:
show: NameA,10,message result: testReturnValue
总结
在实际应用开发中,其实用到反射的地方并不多,更多的是在底层的运行逻辑,如JDBC连接数据库,Spring实例化对象,以及工厂模式中等等,因此,了解其原理还是非常有必要的。