二、Java反射API
Java中以反射(reflection)API实现了Java语言的自省,关于自省和反射的区别,笔者无法给出明确的定义。反射API使得Java语言更易实现运行时的动态性,获取Java程序在运行时刻的内部结构,如Java列中的构造方法、域和方法等。反射API的主要功能包括:
① 确定一个对象的类
② 取出类的修饰符(modifiers),字段,方法,构造器和超类
③ 找出某个接口里定义的常量和方法说明
④ 创建一个类实例,这个实例在运行时刻才有名字(运行时间才生成的对象)
⑤ 取得和设定对象数据成员的值,如果数据成员名是运行时刻确定的也能做倒。
⑥ 在运行时刻调用动态对象的方法
⑦ 创建数组,数组大小和类型在运行时刻才确定。也能更改数组成员的值。
1. 对类的分析
① 获取对象的类(Class对象)
Class c = obj.getClass();
② 获取一个类的超类
Class sc = c.getSuperclass();
下面一段代码打印一个对象所有的父类
③ 获取类的名字
String name = c.getName();
④ 获取类的修饰符
int m = c.getModifiers();
此处getModifiers()方法返回的是个整型的结果,结果m并不具体表示哪一种修饰符。先看java.lang.reflect.Modifier类,该类对修饰符进行了相应的包装。
类Modifier中定义了一系列的类描述符常量:Modifier.ABSTRACT、Modifier.FINAL、Modifier.PUBLIC等。getModifiers()方法返回的即是类的所有修饰符分别对应的整型值的或运算结果,如public abstract class MyClass{},new MyClass().getClass().getModfiers()即得到Modifier.ABSTRACT | Modifier.PUBLIC。
同时Modifier类提供了一些列的方法,用于判断getModifiers()返回的整型数字中包含的类修饰符:Modifier.isPublic(m)、Modifier.isAbstract(m)、Modifier.isFinal(m)等。
⑤ 确定一个类实现的接口
Class[] theInterfaces = c.getInterfaces();
从上面的语句中可以看到,接口在反射的API中也是用Class表示的,接口是一种特殊的抽象类。可以使用Class类的isInterface()方法判断一个Class类型是一个类还是接口。
⑥ 获取类的字段
一个类的字段(Field)可能来自本类、父类、实现的接口或者接口的接口,可以使用Class对象的getFields()方法获取类中所有的public属性的字段的数组(Field对象数组)。Field对象提供方法取得字段的名字、类型和描述符,甚至可以给字段赋值或者取字段的值。当然,Class类对象提供了getDeclaredFields()方法获取包括public属性在内的所有在类中声明了的域。
⑦ 获取构造方法
构造方法是在创建类对象时调用的特殊方法,构造方法可以重载,由它们的参数加以区别。调用getConstructors方法可以取得类构造方法的有关信息,这个方法返回一个数组的
Constructor对象。可以用 Constructor对象里的相关方法来确定构造方法的名字、描述符、参数类型和抛出的意外列表。也可以用Constructor.newInstance创建一个新的Constructor对象。
⑧ 获取成员方法
如何找出类的public方法呢?当然是调用getMethods方法。由getMethods方法返回一个数组,数组元素类型是Method对象。方法的名字,类型,参数,描述和抛出的意外都可
以由Method对象的方法来取得。用 Method.invoke 方法自己调用这个方法。
Method类的对象可以通过getName取方法名、getReturnType取返回值的类型、用
getParameterTypes取得参数类型(Class对象)的数组,对每个参数用getName取参数的类型名。
同样,对于非public的方法,可以使用getDeclaredMethods()方法获取。
2. 实现程序的动态性
① 创建对象
如果知道一个类型,很容易使用new操作符创建一个类的实例。但是如果在编译时并不知道具体要实例化的是哪个类的对象,如何创建该实例呢?
Java中提供Class.forName(String className)从一个字符串(含包的类全名称)加载一个类,再利用newInstance方法创建该类的实例。
当然,也可以先获取一个类的Constructor,再调用其newInstance方法创建对象。不同的是,constructor对象的newInstance方法需要传递一个对象数组作为构造方法的参数列表。
② 获取/设置字段值
利用Reflection API获取或者设置字段的值,首先要得到Class对象,然后利用Class对象的getField方法取得相应的字段Field对象,然后调用Field对象对应的getXXX/setXXX方法获取或者设置属性的值。Filed对象提供getInt/setInt、getLong/setLong等方法对基本类型的属性进行值的获取和设置,可以直接使用get/set方法获取复杂类型属性的值(返回一个对象值/传递一个对象值作为参数)。
具体操作方法见如下代码片段。当然,若试图获取/设置一个非public的字段值(getField方法也不能获取非pubic的属性对象,可以尝试使用getDeclaredField方法),将产生IllegalAccessException,可以通过setAccessible(true)使非public的字段可见,然后对字段值进行访问。
③ 调用方法
方法调用的过程类似于设置字段值的过程,首先要得到Class对象,然后调用getMethod方法得到方法Method的对象,getMethod方法要求传递两个参数,一个是方法名,第二个是Class[],即方法参数列表中各参数对应的类型的数组。然后执行Method对象的invoke方法,进行方法的调用,invoke方法需要传递两个参数,第一个是方法绑定的对象,第二个是方法的参数列表。如果是static的方法,则第一个参数将自动被忽略(可为null)。
同理,对于非public的方法,可以使用getDeclaredMethod方法获取Method对象,并使用setAccessible设置其可见性。
3. 其他操作
① 操作数组
反射API中对数组的操作方式不同于一般的java对象,需要通过专门的java.lang.reflect.Array工具类进行实现。Array类提供了创建和操作数组中元素的方法。Array.newInstance方法用来创建新数组,第一个参数为数组中元素的类型,后面的参数为数组各维度的长度(newInstance为变长参数的方法)。
② 关于权限和异常
使用Java反射API的一个重要好处是可以绕过Java语言中默认的访问控制权限。Constructor、Filed和Method都继承自java.lang.reflect.AccessibleObject,其中的setAccessible方法可以用于设置是否绕过默认的权限检查,否则,访问非public的方法或者字段将产生IllegalAccessException异常。
而在利用Invoke方法来调用方法是,如果方法本身抛出异常,invoke方法将抛出InvocationTargetException异常,通过getCause方法可以获取实际的异常信息。
在Java7中,为所有与反射相关的异常类添加了一个统一的父类java.lang.ReflectiveOperationException,在处理反射相关的异常时可以直接捕获该异常而不用分别捕获各个异常子类。
关于Java动态性,除了反射机制外,Java7中提供了一种新的动态调用Java程序的方法,即方法句柄MethodHandler,有些类似于C中的函数指针。此部分内容将在后续的文章中进行单独的分析。
附件中包含了该文章的测试代码。
Java中以反射(reflection)API实现了Java语言的自省,关于自省和反射的区别,笔者无法给出明确的定义。反射API使得Java语言更易实现运行时的动态性,获取Java程序在运行时刻的内部结构,如Java列中的构造方法、域和方法等。反射API的主要功能包括:
① 确定一个对象的类
② 取出类的修饰符(modifiers),字段,方法,构造器和超类
③ 找出某个接口里定义的常量和方法说明
④ 创建一个类实例,这个实例在运行时刻才有名字(运行时间才生成的对象)
⑤ 取得和设定对象数据成员的值,如果数据成员名是运行时刻确定的也能做倒。
⑥ 在运行时刻调用动态对象的方法
⑦ 创建数组,数组大小和类型在运行时刻才确定。也能更改数组成员的值。
1. 对类的分析
① 获取对象的类(Class对象)
Class c = obj.getClass();
② 获取一个类的超类
Class sc = c.getSuperclass();
下面一段代码打印一个对象所有的父类
- // 分析类的父类
- public static void printSuperclasses(Object o) {
- Class subclass = o.getClass();
- Class superclass = subclass.getSuperclass();
- while (superclass != null) {
- String className = superclass.getName();
- System.out.println(className);
- subclass = superclass;
- superclass = subclass.getSuperclass();
- }
- }
③ 获取类的名字
String name = c.getName();
- // 分析对象的类
- public static void classAnalysis(Object obj) {
- // 获得对象的类
- Class c = obj.getClass();
- System.out.println(obj + " is instance of class: " + c.getName());
- // 获得对象所属类的父类
- Class sc = c.getSuperclass();
- System.out.println(c.getName() + " inherits from class: "
- + sc.getName());
- }
④ 获取类的修饰符
int m = c.getModifiers();
此处getModifiers()方法返回的是个整型的结果,结果m并不具体表示哪一种修饰符。先看java.lang.reflect.Modifier类,该类对修饰符进行了相应的包装。
类Modifier中定义了一系列的类描述符常量:Modifier.ABSTRACT、Modifier.FINAL、Modifier.PUBLIC等。getModifiers()方法返回的即是类的所有修饰符分别对应的整型值的或运算结果,如public abstract class MyClass{},new MyClass().getClass().getModfiers()即得到Modifier.ABSTRACT | Modifier.PUBLIC。
同时Modifier类提供了一些列的方法,用于判断getModifiers()返回的整型数字中包含的类修饰符:Modifier.isPublic(m)、Modifier.isAbstract(m)、Modifier.isFinal(m)等。
- // 分析类的修饰符
- public static void printModifiers(Object obj) {
- System.out.print("Modifier: ");
- Class c = obj.getClass();
- int m = c.getModifiers();
- if (Modifier.isPublic(m))
- System.out.print("public ");
- if (Modifier.isAbstract(m))
- System.out.print("abstract ");
- if (Modifier.isFinal(m))
- System.out.print("final ");
- System.out.println(c.getName());
- }
⑤ 确定一个类实现的接口
Class[] theInterfaces = c.getInterfaces();
从上面的语句中可以看到,接口在反射的API中也是用Class表示的,接口是一种特殊的抽象类。可以使用Class类的isInterface()方法判断一个Class类型是一个类还是接口。
- // 分析类的接口
- public static void printInterfaceNames(Object o) {
- Class c = o.getClass();
- Class[] theInterfaces = c.getInterfaces();
- for (int i = 0; i < theInterfaces.length; i++) {
- String interfaceName = theInterfaces[i].getName();
- System.out.println(interfaceName);
- }
- }
- // 判断接口
- public static void verifyInterface(Class c) {
- String name = c.getName();
- if (c.isInterface()) {
- System.out.println(name + " 是接口.");
- } else {
- System.out.println(name + " 是类.");
- }
- }
⑥ 获取类的字段
一个类的字段(Field)可能来自本类、父类、实现的接口或者接口的接口,可以使用Class对象的getFields()方法获取类中所有的public属性的字段的数组(Field对象数组)。Field对象提供方法取得字段的名字、类型和描述符,甚至可以给字段赋值或者取字段的值。当然,Class类对象提供了getDeclaredFields()方法获取包括public属性在内的所有在类中声明了的域。
- // 分析类的字段
- public static void printFieldNames(Object o) {
- Class c = o.getClass();
- //public的字段
- Field[] publicFields = c.getFields();
- for (int i = 0; i < publicFields.length; i++) {
- String fieldName = publicFields[i].getName();
- Class typeClass = publicFields[i].getType();
- String fieldType = typeClass.getName();
- System.out.println("字段名: " + fieldName + ", 类型: " + fieldType);
- }
- //所有的字段
- Field[] allFields = c.getDeclaredFields();
- for (int i = 0; i < allFields.length; i++) {
- String fieldName = allFields[i].getName();
- Class typeClass = allFields[i].getType();
- String fieldType = typeClass.getName();
- System.out.println("字段名: " + fieldName + ", 类型: " + fieldType);
- }
- }
⑦ 获取构造方法
构造方法是在创建类对象时调用的特殊方法,构造方法可以重载,由它们的参数加以区别。调用getConstructors方法可以取得类构造方法的有关信息,这个方法返回一个数组的
Constructor对象。可以用 Constructor对象里的相关方法来确定构造方法的名字、描述符、参数类型和抛出的意外列表。也可以用Constructor.newInstance创建一个新的Constructor对象。
- //分析类的构造方法
- public static void showConstructors(Object o) {
- Class c = o.getClass();
- Constructor[] theConstructors = c.getConstructors();
- for (int i = 0; i < theConstructors.length; i++) {
- System.out.print(theConstructors[i].getName());
- System.out.print(" ( ");
- Class[] parameterTypes = theConstructors[i].getParameterTypes();
- for (int k = 0; k < parameterTypes.length; k++) {
- String parameterString = parameterTypes[k].getName();
- System.out.print(parameterString + " ");
- }
- System.out.println(")");
- }
- }
⑧ 获取成员方法
如何找出类的public方法呢?当然是调用getMethods方法。由getMethods方法返回一个数组,数组元素类型是Method对象。方法的名字,类型,参数,描述和抛出的意外都可
以由Method对象的方法来取得。用 Method.invoke 方法自己调用这个方法。
Method类的对象可以通过getName取方法名、getReturnType取返回值的类型、用
getParameterTypes取得参数类型(Class对象)的数组,对每个参数用getName取参数的类型名。
同样,对于非public的方法,可以使用getDeclaredMethods()方法获取。
- //分析类中的成员方法
- public static void showMethods(Object o) {
- Class c = o.getClass();
- Method[] theMethods = c.getMethods();
- for (int i = 0; i < theMethods.length; i++) {
- String methodString = theMethods[i].getName();
- System.out.println("Name: " + methodString);
- String returnString = theMethods[i].getReturnType().getName();
- System.out.println(" Return Type: " + returnString);
- Class[] parameterTypes = theMethods[i].getParameterTypes();
- System.out.print(" Parameter Types:");
- for (int k = 0; k < parameterTypes.length; k++) {
- String parameterString = parameterTypes[k].getName();
- System.out.print(" " + parameterString);
- }
- System.out.println();
- }
- }
2. 实现程序的动态性
① 创建对象
如果知道一个类型,很容易使用new操作符创建一个类的实例。但是如果在编译时并不知道具体要实例化的是哪个类的对象,如何创建该实例呢?
Java中提供Class.forName(String className)从一个字符串(含包的类全名称)加载一个类,再利用newInstance方法创建该类的实例。
- //动态创建类对象
- public static Object createObject(String className) {
- Object object = null;
- try {
- Class classDefinition = Class.forName(className);
- object = classDefinition.newInstance();
- } catch (InstantiationException e) {
- System.out.println(e);
- } catch (IllegalAccessException e) {
- System.out.println(e);
- } catch (ClassNotFoundException e) {
- System.out.println(e);
- }
- return object;
- }
当然,也可以先获取一个类的Constructor,再调用其newInstance方法创建对象。不同的是,constructor对象的newInstance方法需要传递一个对象数组作为构造方法的参数列表。
- //使用Constructor动态创建对象
- public static Object createObject(Constructor constructor, Object[] arguments) {
- System.out.println("Constructor: " + constructor.toString());
- Object object = null;
- try {
- object = constructor.newInstance(arguments);
- System.out.println("Object: " + object.toString());
- return object;
- } catch (InstantiationException e) {
- System.out.println(e);
- } catch (IllegalAccessException e) {
- System.out.println(e);
- } catch (IllegalArgumentException e) {
- System.out.println(e);
- } catch (InvocationTargetException e) {
- System.out.println(e);
- }
- return object;
- }
② 获取/设置字段值
利用Reflection API获取或者设置字段的值,首先要得到Class对象,然后利用Class对象的getField方法取得相应的字段Field对象,然后调用Field对象对应的getXXX/setXXX方法获取或者设置属性的值。Filed对象提供getInt/setInt、getLong/setLong等方法对基本类型的属性进行值的获取和设置,可以直接使用get/set方法获取复杂类型属性的值(返回一个对象值/传递一个对象值作为参数)。
具体操作方法见如下代码片段。当然,若试图获取/设置一个非public的字段值(getField方法也不能获取非pubic的属性对象,可以尝试使用getDeclaredField方法),将产生IllegalAccessException,可以通过setAccessible(true)使非public的字段可见,然后对字段值进行访问。
- // 获取字段值
- static void getFiledValue(Object o, String filedName) {
- Class c = o.getClass();
- Field filed;
- Object value;
- try {
- filed = c.getField(filedName);
- // filed = c.getDeclaredField(filedName);
- // filed.setAccessible(true); //修改字段访问权限
- value = filed.get(o); //可使用getInt、getLong等(若知晓字段基本类型)
- System.out.println(filedName + ": " + value.toString());
- } catch (NoSuchFieldException e) {
- System.out.println(e);
- } catch (SecurityException e) {
- System.out.println(e);
- } catch (IllegalAccessException e) {
- System.out.println(e);
- }
- }
- // 修改字段值
- public static void setFieldValue(Object o, String filedName, Object value) {
- Field filed;
- Class c = o.getClass();
- try {
- filed = c.getField(filedName);
- // filed = c.getDeclaredField(filedName);
- // filed.setAccessible(true); //修改字段访问权限
- filed.set(o, value); //可使用setInt、setLong等(若知晓字段基本类型)
- } catch (NoSuchFieldException e) {
- System.out.println(e);
- } catch (IllegalAccessException e) {
- System.out.println(e);
- }
- }
③ 调用方法
方法调用的过程类似于设置字段值的过程,首先要得到Class对象,然后调用getMethod方法得到方法Method的对象,getMethod方法要求传递两个参数,一个是方法名,第二个是Class[],即方法参数列表中各参数对应的类型的数组。然后执行Method对象的invoke方法,进行方法的调用,invoke方法需要传递两个参数,第一个是方法绑定的对象,第二个是方法的参数列表。如果是static的方法,则第一个参数将自动被忽略(可为null)。
同理,对于非public的方法,可以使用getDeclaredMethod方法获取Method对象,并使用setAccessible设置其可见性。
- //动态调用方法
- public static Object callMethod(Object o, String methodName,
- Class paramsType[], Object paramsValue[]) {
- Object result = null;
- Class c;
- Method method;
- try {
- c = o.getClass();
- method = c.getMethod(methodName, paramsType);
- // method = c.getDeclaredMethod(methodName, paramsType);
- // method.setAccessible(true);
- result = method.invoke(o, paramsValue);
- } catch (NoSuchMethodException e) {
- System.out.println(e);
- } catch (IllegalAccessException e) {
- System.out.println(e);
- } catch (InvocationTargetException e) {
- System.out.println(e);
- }
- return result;
- }
3. 其他操作
① 操作数组
反射API中对数组的操作方式不同于一般的java对象,需要通过专门的java.lang.reflect.Array工具类进行实现。Array类提供了创建和操作数组中元素的方法。Array.newInstance方法用来创建新数组,第一个参数为数组中元素的类型,后面的参数为数组各维度的长度(newInstance为变长参数的方法)。
- // 反射API操作数组
- public static void useArray() {
- String[] names = (String[]) Array.newInstance(String.class, 10);
- names[0] = "Hello";
- Array.set(names, 1, "World");
- String str = (String) Array.get(names, 0);
- int[][][] matrix1 = (int[][][]) Array.newInstance(int.class, 3, 3, 3);
- matrix1[0][0][0] = 1;
- int[][][] matrix2 = (int[][][]) Array.newInstance(int[].class, 3, 4);
- matrix2[0][0] = new int[10];
- matrix2[0][1] = new int[3];
- matrix2[0][0][1] = 1;
- }
② 关于权限和异常
使用Java反射API的一个重要好处是可以绕过Java语言中默认的访问控制权限。Constructor、Filed和Method都继承自java.lang.reflect.AccessibleObject,其中的setAccessible方法可以用于设置是否绕过默认的权限检查,否则,访问非public的方法或者字段将产生IllegalAccessException异常。
而在利用Invoke方法来调用方法是,如果方法本身抛出异常,invoke方法将抛出InvocationTargetException异常,通过getCause方法可以获取实际的异常信息。
在Java7中,为所有与反射相关的异常类添加了一个统一的父类java.lang.ReflectiveOperationException,在处理反射相关的异常时可以直接捕获该异常而不用分别捕获各个异常子类。
关于Java动态性,除了反射机制外,Java7中提供了一种新的动态调用Java程序的方法,即方法句柄MethodHandler,有些类似于C中的函数指针。此部分内容将在后续的文章中进行单独的分析。
附件中包含了该文章的测试代码。