前言
本章主要介绍 Java 反射的定义,反射相关类,以及反射 API 的使用。
推荐资料:韩顺平 Java 反射专题
版本约定
- JDK Version:11.0.12
- Java SE API Documentation:https://docs.oracle.com/en/java/javase/11/docs/api/index.html
正文
反射的定义
Java 反射机制允许程序在执行期间借助反射 API 获取任何类的内部信息(比如成员变量,构造器,成员方法等),并能操作对象的属性和方法。反射在设计模式和框架底层都会用到。
在加载完某个类之后,在堆中就会产生一个 Class 类型的对象(一个类只有一个 Class 对象),这个对象包含了某个类的完整结构信息。通过这个对象就可以得到类的结构。这个对象就像一面镜子,透过这个镜子就可以看到类的结构,所以,形象的称之为反射。
关于反射的用途和缺点,参考官方文档:https://docs.oracle.com/javase/tutorial/reflect/index.html
反射机制原理图
Java 程序在计算机中有三个阶段,分别是编译阶段、加载阶段、运行阶段,通常我们更加熟悉运行阶段。
比如我们编写了一个 Cat.java 类,它的内容如下所示:
public class Cat {
private String name;
public Cat() {}
public void hi() {}
}
通过 javac 命令完成 Cat 类的编译,生成一个 Cat.class 的字节码文件。
当我们运行Cat cat = new Cat()
代码的时候,类加载器(ClassLoader)会将 Cat 类的字节码文件加载到堆内存中,生成一个 Class 类对象,这个 Class 类对象包含了 Cat 类的完整结构信息(成员变量、构造器、成员方法等等)。类加载器的这一动作就提现了 Java 的反射机制。
当 Cat 类加载完后,就会生成一个 Cat 对象,这个对象也存在在堆内存中,并且该对象知道它是属于那个 Class 类对象的,可以简单理解成 Cat 对象和 Class 类对象之间存在映射关系,所以,我们通过cat.getClass()
方法可以得到它的 Class 类对象。
上图可以分为两部分,左边部分 JVM 完成类的加载工作(字节码编译不属于 JVM 范畴),右边部分属于应用,我们在拿到某个类的 Class 类对象之后,可以创建这个类的对象,调用这个类的方法,操作这个类的属性等。
反射主要类
反射相关的主要类有如下几个:
- java.lang.Class:代表一个类;
- java.lang.reflect.Method:代表类的成员方法;
- java.lang.reflect.Field:代表类的成员变量;
- java.lang.reflect.Constructor:代表类的构造方法。
Class 类
Class 类是我们使用反射功能的入口,一切反射的使用都要从获得一个 Class 类对象开始。
引用一段《Java 核心技术 卷1 基础知识》中描述 Class 类的内容:
在程序运行期间,Java 运行时系统始终为所有的对象维护一个被称为运行时的类型标识。这个信息跟踪着每个对象所属的类。虚拟机利用运行时类型信息选择相应的方法执行。
可以通过专门的 Java 类访问这些信息,保存这些信息的类被称为 Class。
Class 类具有如下几种特性:
- Class 类和其他类是一样的,都继承了 Object 类,只是这个类的功能有一些特殊,它的类图如下所示。
- Class 类对象不是 new 出来的,而是 JVM 创建的。通过上面的反射机制原理图,可以知道 Class 类对象是通过类加载器(ClassLoader)生成的。
Debug 上面的代码,Force Step Into,可以进入到 ClassLoader.loadClass 方法中。
对于某个类的 Class 类对象,在内存中只有一份,因为类只加载一次。关于这点,也好解释,上面的代码,如果我们继续对反射方式 Debug 的时候,就不会再次调用 ClassLoader.loadCalss 方法了,因为传统方式已经加载了 Cat 类。另外,通过下面的代码也可以看出来。
public static void main(String[] args) throws ClassNotFoundException {
Class<?> cat1 = Class.forName("test15.Cat");
Class<?> cat2 = Class.forName("test15.Cat");
System.out.println("cat1.hashCode(): " + cat1.hashCode());
System.out.println("cat2.hashCode(): " + cat2.hashCode());
}
运行程序,输出:
cat1.hashCode(): 403424356
cat2.hashCode(): 403424356
两个对象的 hashCode 一样。
每个类的对象都知道自己是由哪个 Class 类对象生成的。
public static void main(String[] args) {
Cat cat = new Cat();
Class clz = cat.getClass();
}
比如可以通过 Cat 类对象获取它对应的 Class 类对象。
通过 Class 类对象可以得到一个类的完整结构。
- Class 类对象是存放在堆中的。
- 类的字节码二进制数据,存放在方法区。
获取 Class 类对象的几种方式:
- 已知某个类的全类名,且该类在类路径下,可通过 Class 类的静态方法 forName() 获取 Class 类对象,可能抛出 ClassNotFoundException;
Class cls1 = Class.forName("test1.Cat");
- 已知具体的类,通过类的 class 获取 Class 类对象,该方式最为安全可靠,且程序性能最高;
Class cls2 = Cat.class;
- 已知某个类的实例,调用该实例的 getClass() 方法获取 Class 类对象;
Class cls3 = 对象.getClass();
- 通过类加载器获取 Class 类对象;
ClassLoader cl = 对象.getClass().getClassLoader();
Class cls4 = cl.loadClass("类的全类名");
- 基本数据类型(int,char,boolean,float,double,byte,long,short)和数组类型按如下方式得到 Class 类对象;
Class cls5_1 = 基本数据类型.class;
Class cls5_2 = 数组类型.class;
- 基本数据类型对应的包装类,可通过 TYPE 得到 Class 类对象;
Class cls6 = 包装类.TYPE;
常用的就前面三种方式。public static void main(String[] args) throws ClassNotFoundException {
Class cls1 = Class.forName("test1.Cat");
Class cls2 = Cat.class;
Class cls3 = new Cat().getClass();
Class cls4 = Test9.class.getClassLoader().loadClass("test1.Cat");
Class cls5_1 = int.class;
Class cls5_2 = int[].class;
Class cls6 = Integer.TYPE;
}
Class 类的方法比较多,常用的方法就这么几种:
- 获取 Class 类对象:forName
- 获得实体的名称:getName
- 动态地创建一个类的实例:newInstance
- 获得实体的成员变量(Field)相关的方法:getFields,getField,getDeclaredFields,getDeclaredField
- 获得实体的成员方法(Method)相关的方法:getMethods,getMethod,getDeclaredMethods,getDeclaredMethod
- 获得实体的构造函数(Constructor)相关的方法:getConstructors,getDeclaredConstructors,getConstructor,getDeclaredConstructor
以上方法的具体使用以及其他方法的介绍参考:Class 类
其他类
反射的其他主要类 Field,Method 以及 Constructor 都是通过 Class 类获取的,我们在案例中介绍。
案例
利用反射分析类的能力
在 java.lang.reflect 包中有三个类 Field、Method 和 Constructor 分别用于描述类的域、方法和构造器。
Class 类中的 getFields、getMethods 和 getConstructors 方法将分别返回类提供的 public 域、方法和构造器数组,其中包括超类的共有成员。
Class 类的 getDeclareFields、getDeclareMethods 和 getDeclaredConstructors 方法将分别返回类中声明的全部域、方法和构造器,其中包括私有和受保护成员,但不包括超类的成员。
Field、Method 和 Constructor 都提供了哪些常见的方法用来分析类的能力?
- Field、 Method 和 Constructor 都有一个叫做 getName 的方法, 用来返回项目的名称;
- Field 类有一个 getType 方法, 用来返回描述域所属类型的 Class 对象;
- Method 和 Constructor 类有能够获取方法参数类型的方法,Method 类还有一个可以获取方法返回类型的方法;
- 这三个类还有一个叫做 getModifiers 的方法,它将返回一个整型数值,用不同的位开关描述 public 和 static 这样的修饰符使用状况。
- 可以利用 java.lang.reflect 包中的 Modifier 类的静态方法分析 getModifiers 方法返回的整型数值。例如, 可以使用 Modifier 类中的 isPublic、 isPrivate 或 isFinal 判断方法或构造器是否是 public、 private 或 final。
接下来,我们利用反射,打印出一个类的全部信息(域、方法和构造器)。
public class ReflectionTest {
public static void main(String[] args) throws ClassNotFoundException {
String name = "java.lang.Double";
Class clazz = Class.forName(name);
Class superClazz = clazz.getSuperclass();
String modifiers = Modifier.toString(clazz.getModifiers());
if (modifiers.length() > 0) {
System.out.print(modifiers + " ");
}
System.out.print("class " + name);
if (superClazz != null && superClazz != Object.class) {
System.out.print(" extends " + superClazz.getName());
}
System.out.print("\n{\n");
printConstructors(clazz);
System.out.println();
printMethods(clazz);
System.out.println();
printFields(clazz);
System.out.println("}");
}
/**
* Prints all constructors of a class
*
* @param clazz
*/
public static void printConstructors(Class clazz) {
Constructor[] constructors = clazz.getDeclaredConstructors();
for (Constructor constructor : constructors) {
String name = constructor.getName();
System.out.print(" ");
String modifiers = Modifier.toString(constructor.getModifiers());
if (modifiers.length() > 0) {
System.out.print(modifiers + " ");
}
System.out.print(name + "(");
Class[] paramTypes = constructor.getParameterTypes();
for (int i = 0; i < paramTypes.length; i++) {
if (i > 0) {
System.out.print(", ");
}
System.out.print(paramTypes[i].getName());
}
System.out.println(");");
}
}
/**
* Prints all methods of a class
*
* @param clazz
*/
public static void printMethods(Class clazz) {
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
Class returnType = method.getReturnType();
String name = method.getName();
System.out.print(" ");
String modifiers = Modifier.toString(method.getModifiers());
if (modifiers.length() > 0) {
System.out.print(modifiers + " ");
}
System.out.print(returnType.getName() + " " + name + "(");
Class[] paramTypes = method.getParameterTypes();
for (int i = 0; i < paramTypes.length; i++) {
if (i > 0) {
System.out.print(", ");
}
System.out.print(paramTypes[i].getName());
}
System.out.println(");");
}
}
/**
* Prints all fields of a class
*
* @param clazz
*/
public static void printFields(Class clazz) {
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
Class type = field.getType();
String name = field.getName();
System.out.print(" ");
String modifiers = Modifier.toString(type.getModifiers());
if (modifiers.length() > 0) {
System.out.print(modifiers + " ");
}
System.out.println(type.getName() + " " + name + ";");
}
}
}
运行程序,输出:
public final class java.lang.Double extends java.lang.Number
{
public java.lang.Double(double);
public java.lang.Double(java.lang.String);
public boolean equals(java.lang.Object);
public static java.lang.String toString(double);
public java.lang.String toString();
public int hashCode();
public static int hashCode(double);
public static double min(double, double);
public static double max(double, double);
public static native long doubleToRawLongBits(double);
public static long doubleToLongBits(double);
public static native double longBitsToDouble(long);
public volatile int compareTo(java.lang.Object);
public int compareTo(java.lang.Double);
public byte byteValue();
public short shortValue();
public int intValue();
public long longValue();
public float floatValue();
public double doubleValue();
public static java.lang.Double valueOf(java.lang.String);
public static java.lang.Double valueOf(double);
public static java.lang.String toHexString(double);
public static int compare(double, double);
public static boolean isNaN(double);
public boolean isNaN();
public static boolean isFinite(double);
public static boolean isInfinite(double);
public boolean isInfinite();
public static double sum(double, double);
public static double parseDouble(java.lang.String);
public abstract final double POSITIVE_INFINITY;
public abstract final double NEGATIVE_INFINITY;
public abstract final double NaN;
public abstract final double MAX_VALUE;
public abstract final double MIN_NORMAL;
public abstract final double MIN_VALUE;
public abstract final int MAX_EXPONENT;
public abstract final int MIN_EXPONENT;
public abstract final int SIZE;
public abstract final int BYTES;
public final java.lang.Class TYPE;
public abstract final double value;
public abstract final long serialVersionUID;
}
Process finished with exit code 0
利用反射分析对象
从前面一节中,我们已经知道如何利用 Field、Method 和 Constructor 中提供的方法来分析类的能力。本节将进一步利用这三个反射类来分析对象。
修改一下 Cat 类,下面继续使用该类演示反射功能。
public class Cat {
private String name = "小黑";
public int age = 1;
public Cat() {
}
private Cat(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return this.name;
}
public int getAge() {
return age;
}
private void setName(String name) {
this.name = name;
}
public static void printCat(String name, int age) {
System.out.println("猫的名字:" + name + ",猫的年龄:" + age);
}
@Override
public String toString() {
return "Cat{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
利用 Constructor 类中的方法动态的创建类的实例
- 首先,我们需要先获得某个 Constructor 对象;
通过这个 Constructor 对象创建指定类的实例。
public static void main(String[] args) throws Exception {
Class cls = Cat.class;
// 1.获得无参Constructor对象
Constructor constructor1 = cls.getConstructor();
// 利用无参构造函数创建Cat类的实例
Cat cat1 = (Cat) constructor1.newInstance(null);
// 会调用cat的toString方法
System.out.println(cat1);
// 2.获得有参私有Constructor对象
Constructor constructor2 = cls.getDeclaredConstructor(String.class, int.class);
// 利用有参构造函数创建Cat类的实例
Cat cat2 = (Cat) constructor2.newInstance("小白", 2);
// 会调用cat的toString方法
System.out.println(cat2);
}
运行程序,输出:
Cat{name='小黑', age=1}
Exception in thread "main" java.lang.IllegalAccessException: class test1.Test10 cannot access a member of class test1.Cat with modifiers "private"
at java.base/jdk.internal.reflect.Reflection.newIllegalAccessException(Reflection.java:361)
at java.base/java.lang.reflect.AccessibleObject.checkAccess(AccessibleObject.java:591)
at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:481)
at test1.Test10.main(Test10.java:24)
我们已经注意到在获取私有的构造函数的时候需要使用
getDeclaredConstructor
方法,但是上面的代码任然抛出 IllegalAccessException 异常,什么原因?
Java 程序受到安全管理器的控制,只允许查看任意对象有哪些构造函数,而不允许调用 Constructor 的 newInstance 方法创建类的实例。同样的,Field 的 get 方法和 Method 的 invoke 方法同样存在这样的问题。
为了让 Java 程序不受到安全管理器的控制,需要调用 Field、Method 或 Constructor 对象的 setAccessible 方法。setAccessible 方法是 AccessibleObject 类中的一个方法,它是 Field、Method 和 Constructor 类的公共超类。
比如上面的代码可以添加:constructor2.setAccessible(true);
public static void main(String[] args) throws Exception {
Class cls = Cat.class;
// 1.获得无参Constructor对象
Constructor constructor1 = cls.getConstructor();
// 利用无参构造函数创建Cat类的实例
Cat cat1 = (Cat) constructor1.newInstance(null);
// 会调用cat的toString方法
System.out.println(cat1);
// 2.获得有参私有Constructor对象
Constructor constructor2 = cls.getDeclaredConstructor(String.class, int.class);
// 利用有参构造函数创建Cat类的实例
constructor2.setAccessible(true);
Cat cat2 = (Cat) constructor2.newInstance("小白", 2);
// 会调用cat的toString方法
System.out.println(cat2);
}
运行程序,输出:
Cat{name='小黑', age=1}
Cat{name='小白', age=2}
这样就不会抛出异常了。
利用 Field 类中的方法操作域
- 首先,我们需要先获得某个域的 Field 对象;
通过这个 Field 对象操作指定的域。
public static void main(String[] args) throws Exception {
Cat cat = new Cat();
Class cls = Cat.class;
// 1.获得共有字段age的Field对象
Field ageField = cls.getField("age");
// age是int类型,可以使用Field类中的getInt方法
int ageVal = ageField.getInt(cat);
System.out.println("第1次 age:" + ageVal);
// 使用setInt方法重设age的值
ageField.setInt(cat, 2);
ageVal = ageField.getInt(cat);
System.out.println("第2次 age:" + ageVal);
// 2.获得私有字段name的Field对象
Field nameField = cls.getDeclaredField("name");
// 同样私有方法需要设置setAccessible方法,获得访问权
nameField.setAccessible(true);
// name是String类型,可以使用Field类中的get方法
String nameVal = (String) nameField.get(cat);
System.out.println("第1次 name:" + nameVal);
// 使用set方法重设name的值
nameField.set(cat, "小白");
nameVal = (String) nameField.get(cat);
System.out.println("第2次 name:" + nameVal);
}
运行程序,输出:
第1次 age:1
第2次 age:2
第1次 name:小黑
第2次 name:小白
同样的,name 是一个私有域,所以需要调用 setAccessible 方法,获得访问权。另外,如果域是 int 类型,可以使用 Field 类中的 getInt,setInt 方法操作该域,其他基本类型也有对应的方法。
利用 Method 类中的方法动态的执行实例的方法
- 首先,我们需要先获得某个方法的 Method 对象;
通过这个 Method 对象操作指定的方法。
public static void main(String[] args) throws Exception {
Cat cat = new Cat();
Class cls = Cat.class;
// 1.获得共有方法getName的Method对象
Method method1 = cls.getMethod("getName");
// 执行方法
String name = (String) method1.invoke(cat);
System.out.println("猫的名字:" + name);
// 2.获得私有方法setName的Method对象
Method method2 = cls.getDeclaredMethod("setName", String.class);
// 执行方法
method2.setAccessible(true);
method2.invoke(cat, "小白");
System.out.println(cat);
// 3.获得静态方法printCat的Method对象
Method method3 = cls.getMethod("printCat", String.class, int.class);
// 执行静态方法,不需要传Cat对象
method3.invoke(null, "小白", 2);
}
运行程序,输出:
猫的名字:小黑
Cat{name='小白', age=1}
猫的名字:小白,猫的年龄:2
如果返回类型是基本类型,invoke 方法会返回其包装器类型。比如,method1 表示 Cat 类的 getAge 方法,那么返回的对象实际上是一个 Integer,必须相应地完成类型转换,可以使用自动拆箱将它转换为一个 int:
int age = (Integer) method1.invoke(cat);
如果存在多个相同名字的方法,可以通过方法的参数类型来区分。比如,还存在一个 getName 方法,需要传一个 String 类型的参数,则可以通过cls.getMethod("getName", String.class);
来区分方法。
如果调用的方法是一个静态方法,invoke 方法的第一个参数传 null。
引用一段《Java 核心技术 卷1 基础知识》中描述 Method 类的内容:
上述程序清楚地表明,可以使用 method 对象实现 C 语言中函数指针的所有操作。同 C 一样,这种程序设计风格并不太简便,出错的可能性也比较大。如果在调用方法的时候提供了一个错误的参数,那么 invoke 方法将会抛出一个异常。
另外,invoke 的参数和返回值必须是 Object 类型的。这就意味着必须进行多次的类型转换。这样做将会使编译器错过检查代码的机会。因此,等到测试阶段才会发现这些错误,找到并改正它们将会更加困难。不仅如此,使用反射获得方法指针的代码要比仅仅直接调用方法明显慢一些。
有鉴于此,建议仅在必要的时候才使用 Method 对象,而最好使用接口以及 Java SE 8 中的 lambda 表达式。特别要重申:建议 Java 开发者不要使用 Method 对象的回调功能。使用接口进行回调会使得代码的执行速度更快,更易于维护。
反射的优点和缺点
- 优点:可以动态的创建和使用对象(也是框架底层核心),使用灵活,没有反射机制,框架技术就失去底层支撑。
- 缺点:使用反射基本是解释执行,对执行速度有影响。
我们用一个例子来比较用传统方式调用方法和反射方式调用方法,两者的执行速度是不是差别很大。
public class Test7 {
public static void main(String[] args) throws Exception {
m1();
m2();
}
public static void m1() {
Cat cat = new Cat();
long startTime = System.currentTimeMillis();
for (int i = 0; i < 100000000; i++) {
cat.hi();
}
long endTime = System.currentTimeMillis();
System.out.println("传统方式调用 hi 方法执行时间:" + (endTime - startTime));
}
public static void m2() throws Exception {
Class cls = Class.forName("test15.Cat");
Object o = cls.newInstance();
Method hi = cls.getMethod("hi");
long startTime = System.currentTimeMillis();
for (int i = 0; i < 100000000; i++) {
hi.invoke(o);
}
long endTime = System.currentTimeMillis();
System.out.println("反射方式调用 hi 方法执行时间:" + (endTime - startTime));
}
}
运行程序,输出:
传统方式调用 hi 方法执行时间:3
反射方式调用 hi 方法执行时间:270
对比两者的执行时间,确实差别比较大。有没有办法优化反射的性能?
反射优化的空间有限,可以通过关闭访问检查来优化一些反射的性能。
什么是访问检查?
Method、Field 和 Constructor 对象都有 setAccessible() 方法,setAccessible() 的作用是启用和禁用访问安全检查,当传入方法的参数值为 true 时,表示反射的对象在使用时取消访问检查,提高反射的效率;当传入方法的参数值为 false 时,表示反射的对象在使用时需要执行访问检查。
打开 Field 的类图,它继承了 AccessibleObject 类,在 AccessibleObject 类中有 setAccessible 方法。
修改上面的代码,在调用 hi() 方法之前,先执行 hi.setAccessible(true);
取消访问检查。
public class Test7 {
public static void main(String[] args) throws Exception {
m1();
m2();
}
public static void m1() {
Cat cat = new Cat();
long startTime = System.currentTimeMillis();
for (int i = 0; i < 100000000; i++) {
cat.hi();
}
long endTime = System.currentTimeMillis();
System.out.println("传统方式调用 hi 方法执行时间:" + (endTime - startTime));
}
public static void m2() throws Exception {
Class cls = Class.forName("test15.Cat");
Object o = cls.newInstance();
Method hi = cls.getMethod("hi");
hi.setAccessible(true);
long startTime = System.currentTimeMillis();
for (int i = 0; i < 100000000; i++) {
hi.invoke(o);
}
long endTime = System.currentTimeMillis();
System.out.println("反射方式调用 hi 方法执行时间:" + (endTime - startTime));
}
}
运行程序,输出:
传统方式调用 hi 方法执行时间:3
反射方式调用 hi 方法执行时间:124
参考
作者:殷建卫 链接:https://www.yuque.com/yinjianwei/vyrvkf/lkv999 来源:殷建卫 - 架构笔记 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。