2.2 反射API

2.1 节介绍的Java脚本语言支持API是通过引入其他脚本语言来增强Java平台的动态性,而这一节将要介绍的反射API则是Java语言本身提供的动态支持。通过反射API可以获取Java程序在运行时刻的内部结构,比如Java类中包含的构造方法、域和方法等元素,并可以与这些元素进行交互。通过反射API, Java语言也可以实现很多动态语言所支持的实用而又简洁的功能。下面先通过一个示例来为读者提供一个反射API的直观印象。

按照一般的面向对象的设计思路,一个对象的内部状态都应该通过相应的方法来改变,而不是直接去修改属性的值。一般Java类中的属性设置和获取方法的命名都遵循JavaBeans规范中的要求,即利用setXxx和getXxx这样的方法声明。因此可以实现一个实用工具类来完成对任意对象的属性设置和获取的操作,只要设置和获取属性的方法满足JavaBeans规范。具体的实现方式可以通过与动态语言进行对比来分别介绍。用JavaScript语言来实现这样功能,如代码清单2-14所示。限于篇幅,代码中省略了应有的类型检查和错误处理。

代码清单2-14 设置任意对象的属性值的JavaScript实现


function invokeSetter(obj, property, value){

var funcName="set"+property.substring(0,1).toUpperCase()+property.substring(1);

obj[funcName](value);

}

var obj={

value:0,

setValue:function(val){this.value=val;}

};

invokeSetter(obj,"value",5);


上面的代码只是属性设置方法的示例。代码的逻辑也并不复杂,首先把要设置的属性名称按照JavaBeans规范转换成对应的方法名称,如设置属性“value”的方法名称为“setValue”。由于JavaScript语言本身的特性,方法也是对象的属性,因此可以直接获取到方法后再进行调用。

代码清单2-15给出了使用反射API的Java实现。从代码量上来说,与JavaScript的实现差别并不算大。基本的实现思路也比较直接,先从对象的类中查找到方法,再用传入的参数调用此方法。这个静态方法可以作为一个实用工具方法在程序中使用。

代码清单2-15 使用反射API设置对象的属性值的示例


public class ReflectSetter{

public static void invokeSetter(Object obj, String field, Object value)throws

NoSuchMethodException, InvocationTargetException, IllegalAccessException{

String methodName="set"+field.substring(0,1).toUpperCase()+field.

substring(1);

Class<?>clazz=obj.getClass();

Method method=clazz.getMethod(methodName, value.getClass());

method.invoke(obj, value);

}

}


从上面的示例可以看出,通过反射API, Java语言也可以应用在很多对灵活性要求很高的场景中。从根本上来说,反射API实际上定义了一种功能提供者和使用者之间的松散契约。以方法调用为例,按照Java语言的一般做法,在调用方法的时候,在代码中首先需要一个对象的变量作为调用的接收者,再把方法的名称直接写在代码中。方法的名称不可能是变量。编译器会检查这个对象中是否确实有待调用的方法,如果没有就会出现编译错误。这种一般的做法,是提供者和使用者之间的一种紧密的契约,由编译器来保证其合法性。而使用反射API,两者的契约只需要建立在名称和参数类型这个层次上就足够了。方法名称可以是变量,参数值也可以动态生成。调用的合法性由开发人员自己保证。如果方法调用不是合法的,相关的异常会在运行时抛出。

反射API的一个重要的使用场合是要调用的方法或者要操作的域的名称按照某种规律变化的时候。一个典型的场景就是在Servlet中用HTTP请求的参数值来填充领域对象。比如在用户注册的时候,包含在HTTP请求中的用户所填写的相关信息,需要被填充到程序中定义好的领域对象中。只需要利用Servlet提供的API遍历请求中的所有参数,然后用代码清单2-15中给出的invokeSetter方法设置领域对象中与参数名称相对应的属性的值即可。另外一个场景是在数据库操作中,从SQL查询结果集中创建并填充领域对象。数据库的列名和领域对象的属性名称也存在着类似的对应关系。

反射API在为Java程序带来灵活性的同时,也产生了额外的性能代价。由于反射API的实现机制,对于相同的操作,比如调用同一个方法,用反射API来动态实现比直接在源代码中编写的方式大概慢一到两个数量级。随着Java虚拟机实现的改进,反射API的性能已经有了非常大的提升。但是这种性能的差距是客观存在的。因此,在某些对性能要求比较高的应用中,要慎用反射API。

2.2.1 获取构造方法

通过反射API可以获取到Java类中的构造方法。通过构造方法可以在运行时动态地创建Java对象,而不只是通过new操作符来进行创建。在得到Class类的对象之后,可以通过其中的方法来获取构造方法。相关的方法有4个,其中getConstructors用来获取所有的公开构造方法的列表,getConstructor则根据参数类型来获取公开的构造方法。另外两个对应方法getDeclaredConstructors和getDeclaredConstructor的作用类似,只不过它们会获取类中真正声明的构造方法,而忽略从父类中继承下来的构造方法。得到了表示构造方法的java.lang.reflect.Constructor对象之后,就可以获取关于构造方法的更多信息,以及通过newInstance方法创建出新的对象。

一般的构造方法的获取和使用并没有什么特殊之处,需要特别说明的是对参数长度可变的构造方法和嵌套类(nested class)的构造方法的使用。

如果构造方法声明了长度可变的参数,在获取构造方法的时候,要使用对应的数组类型的Class对象。这是因为长度可变的参数实际上是通过数组来实现的。如代码清单2-16所示,类VarargsConstructor的构造方法包含String类型的可变长度参数,在调用getDeclaredConstructor方法的时候,需要使用String[].class,否则会找不到该构造方法。在调用newInstance的时候,要把作为实际参数的字符串数组先转换成为Object类型,这是为了避免方法调用时的歧义。这样编译器就知道把这个字符串数组作为一个可变长度的参数来传递。

代码清单2-16 使用反射API获取参数长度可变的构造方法


public class VarargsConstructor{

public VarargsConstructor(String……names){}

}

public void useVarargsConstructor()throws Exception{

Constructor<VarargsConstructor>constructor=VarargsConstructor.class.getDeclaredConstructor(String[].class);

constructor.newInstance((Object)new String[]{"A","B","C"});

}


对嵌套类的构造方法的获取,需要区分静态和非静态两种情况,即是否在声明嵌套类的时候使用static关键词。静态的嵌套类并没有特别之处,按照一般的方式来使用即可。而对于非静态嵌套类来说,其特殊之处在于它的对象实例中都有一个隐含的对象引用,指向包含它的外部类对象。也正是这个隐含的对象引用的存在,使非静态嵌套类中的代码可以直接引用外部类中包含的私有域和方法。因此,在获取非静态嵌套类的构造方法的时候,类型参数列表的第一个值必须是外部类的Class对象。如代码清单2-17所示,静态嵌套类StaticNestedClass的使用并没有特殊之处。在获取到非静态嵌套类NestedClass的构造方法之后,用newInstance创建新对象,此时第一个参数就是其外部对象的引用this,与调用getDeclaredConstructor方法时的第一个参数相对应。

代码清单2-17 使用反射API获取嵌套类的构造方法


static class StaticNestedClass{

public StaticNestedClass(String name){}

}

class NestedClass{

public NestedClass(int count){}

}

public void useNestedClassConstructor()throws Exception{

Constructor<Static Nested Class>sncc=Static Nested Class.class.getDeclaredConstructor(String.class);

sncc.newInstance("Alex");

Constructor<NestedClass>ncc=NestedClass.class.getDeclaredConstructor(Const ructorUsage.class, int.class);

NestedClass ic=ncc.newInstance(this,3);

}