Java动态程序设计深入技术介绍:反射

反射授予了你的代码访问装载进JVM内的Java类的内部信息的权限,并且允许你编写在程序执行期间与所选择的类的一同工作的代码,而不是在源代码中。这种机制使得反射成为创建灵活的应用程序的强大工具,但是要小心的是,如果使用不恰当,反射会带来很大的副作用。

在这篇文章中,软件咨询顾问Dennis Sosnoski 介绍了反射的使用,同时还介绍了一些使用反射所要付出的代价。在这里,你可以找到Java反射API是如何在运行时让你钩入对象的。

在第一部分,我向你介绍了Java程序设计的类以及类的装载。那篇文章中描述了很多出现在Java二进制类格式中的信息,现在我来介绍在运行时使用反射API访问和使用这些信息的基础。为了使那些已经了解反射基础的开发人员对这些事情感兴趣,我还会介绍一些反射与直接访问的在性能方面的比较。

使用反射与和metadata(描述其它数据的数据)一些工作的Java程序设计是不同的。通过Java语言反射来访问的元数据的特殊类型是在JVM内部的类和对象的描述。反射使你可以在运行时访问各种类信息,它甚至可以你让在运行时读写属性字段、调用所选择的类的方法。

反射是一个强大的工具,它让你建立灵活能够在运行时组装的代码,而不需要连接组件间的源代码。反射的一些特征也带来一些问题。在这章中,我将会探究在应用程序中不打算使用反射的原因,以为什么使用它的原因。在你了解到这些利弊之后,你就会在好处大于缺点的时候做出决定。

初识class 使用反射的起点总时一个java.lang.Class类的实例。如果你与一个预先确定的类一同工作,Java语言为直接获得Class类的实例提供了一个简单的快捷方式。例如:


当你使用这项技术的时候,所有与装载类有关的工作都发生在幕后。如果你需要在运行时从外部的资源中读取类名,使用上面这种方法是不会达到目的的,相反你需要使用类装载器来查找类的信息,方法如下所示:


// "name" is the class name to load
Class clas = null;
try {
clas = Class.forName(name);
} catch (ClassNotFoundException ex)
{
// handle exception case
}
// use the loaded class


如果类已经装载,你将会找到当前在在的类的信息。如果类还没有被装载,那么类装载器将会装载它,并且返回最近创建的类的实例。

Class对象给予你了所有的用于反射访问类的元数据的基本钩子。这些元数据包括有关类的自身信息,例如象类的包和子类,还有这个类所实现的接口,还包括这个类所定义的构造器、属性字段以及方法的详细信息。后面的这些项是我们在程序设计过种经常使用的,因此在这一节的后面我会给出一些用这些信息来工作的例子。



Constructor getConstructor(Class[] params) 使用指定的参数类型来获得公共的构造器;
Constructor[] getConstructors()
获得这个类的所有构造器;
Constructor getDeclaredConstructor(Class[] params) 使用指定的参数类型来获得构造器(忽略访问的级别)
Constructor[] getDeclaredConstructors() 获得这个类的所有的构造器(忽略访问的级别)

上述的每一种方法都返回一或多个java.lang.reflect.Constructor的实例。Constructor类定义了一个需要一个对象数据做为唯一参数的newInstance方法,然后返回一个最近创建的原始类的实例。



public class TwoString
{
private String m_s1, m_s2;
public TwoString(String s1, String s2)
{
m_s1 = s1;
m_s2 = s2;
}
}

下面的代码显示如何获得TwoString类的构造器,并使用字符串“a”和“b”来创建一个实例:


Class[] types = new Class[]
{
String.class, String.class
};
Constructor cons =
TwoString.class.getConstructor(types);
Object[] args = new Object[]
{
"a", "b"
};
TwoString ts = cons.newInstance(args);

上面的代码忽略了几种可能的被不同的反射方法抛出的异常检查的类型。这些异常在Javadoc API中有详细的描述,因此为简便起见,我会在所有的代码中忽略它们。




使用默认的构造器创建新的实例。

通过反射来查找属性字段



Field getField(String name)
获得由name指定的具有public级别的属性字段
Field getFields()
获得一个类的所有具有public级别的属性字段
Field getDeclaredField(String name)
获得由name指定的被类声明的属性字段
Field getDeclaredFields()
获得由类定义的所有的属性字段

尽管与构造器的调用很相似,但是在提到属性字段的时候,有一个重要的差别:前两个方法返回能过类来访问的公共(public)属性字段的信息(包括那些来自于超类的属性字段),后两个方法返回由类直接声明的所有的属性字段(忽略了属性字段的访问类型)。

下面的代码显示了如何使用属性字段的反射方法,通过指定属性字段名,找到一个对象的int类型的属性字段,并给这个属性字段值加1。


public int incrementField(String name, 
Object obj) throws...
{
Field field =
obj.getClass().getDeclaredField(name);
int value = field.getInt(obj) + 1;
field.setInt(obj, value);
return value;
}

这个方法开始展现一些使用反射所可能带来的灵活性,它优于与一个特定的类一同工作,incrementField方法把要查找的类信息的对象传递给getClass方法,然后直接在那个类中查找命名的属性字段。

Class反射调用访问方法的信息与访问构造器和字段属性的方法非常相似:


Method getMethod(String name,
Class[] params)
--使用指定的参数类型获得由name
参数指定的public类型的方法。
Mehtod[] getMethods()
获得一个类的所有的public类型的方法
Mehtod getDeclaredMethod(String name,
Class[] params)
使用指定的参数类型获得由name
参数所指定的由这个类声明的方法。
Method[] getDeclaredMethods()
获得这个类所声明的所有的方法

与属性字段的调用一样,前两个方法返回通过这个类的实例可以访问的public类型的方法?包括那些继承于超类的方法。后两个方法返回由这个类直接声明的方法的信息,而不管方法的访问类型。

下面给出了比属性字段的例子更加深入的例子,它显示了一个的方法反射的例子,这个方法使用get和set方法来给JavaBean定义的int类型的属性做增量操作。例如,如果对象为一个整数类型count属性定义了getCount和setCount方法,那么为了给这个属性做增量运算,你就可以把“count”做为参数名传递给调用的这个方法中。示例代码如下:


public int incrementProperty
(String name, Object obj)
{
String prop = Character.
toUpperCase(name.charAt(0)) +
name.substring(1);
String mname = "get" + prop;
Class[] types = new Class[] {};
Method method =
obj.getClass().getMethod(mname, types);
Object result =
method.invoke(obj, new Object[0]);
int value =
((Integer)result).intValue() + 1;
mname = "set" + prop;
types = new Class[]
{
int.class
};
method = obj.getClass().
getMethod(mname, types);
method.invoke(obj, new Object[]
{
new Integer(value)
}
);
return value;
}

根据JavaBeans的规范,我把属性名的第一个字母转换为大写,然后在前面加上“get”来建立读取属性值的方法名,在属性名前加上“set”来建立设置属性值的方法名。

最后规范规定这两个方法应该是public类型的,因此我使用了查找相关类的public类型方法的调用形式。

这种方法可应用于调用和返回。因此在我的例子中调用get方法时,我预期的结果是一个由java.lang.Integer类所封装的实际的int类型的属性值。

在Java语言中数组是对象,象其它所有的对象一样,它有一些类。如果你有一个数组,你可以和其它任何对象一样使用标准的getClass方法来获得这个数组的类,但是你获得的这个类与其它的对象类型相比,不同之处在它没有一个现存的工作实例。

数组特殊处理要使用java.lang.reflect.Array类提供的一个静态方法的集合,这个类中的方法可以让你创建新的数组,获得一个数组对象的长度,以及读写一个数组对象的索引值。



public Object growArray
(Object array, int size)
{
Class type = array.getClass
().getComponentType();
Object grown =
Array.newInstance(type, size);
System.arraycopy(array, 0, grown, 0,
Math.min(Array.getLength(array),
size));
return grown;
}

安全与反射

因为这些冲突的需要,Java语言定义了一个多级方法来处理反射安全。基本的模式是在反射请求源码访问的时候强制使用如下相同的约束限制:

不访问这个类本身外部的private组件;

围绕这些限制有一个简单的方法,我在前面的例子中所使用的所有构造器、属性字段、以及类的方法都扩展于一个共同的基类???java.lang.reflect.AccessibleObject类。这个类定义了一个setAccessible方法,这个方法可以让你打开或关闭这些对类的实例的访问检查。如果安全管理器被设置为关闭访问检查,那么就允许你访问,否则不允许,安全管理器会抛出一个异常。



public class ReflectSecurity
{
public static void main(String[] args)
{
try {
TwoString ts =
new TwoString("a", "b");
Field field =
clas.getDeclaredField("m_s1");
// field.setAccessible(true);
System.out.println
("Retrieved value is " +
field.get(inst));
}
catch (Exception ex)
{
ex.printStackTrace(System.out);
}
}
}

如果你编译这段代码并且直接使用不带任何参数的命令行命令来运行这个程序,它会抛出一个关于field.get(inst)调用的IllegalAccessException异常,如果你去掉上面代码中field.setAccessible(true)行的注释,然后编译并重新运行代码,它就会成功执行。

反射性能

下面列出一段来自于属性字段的访问性能测试的摘要,它包括基本的测试方法。每个方法测试一种访问属性字段的形式,accessSame方法和本对象的成员字段一起工作,accessReference方法直接使用另外的对象属性字段来存取,accessReflection通过反射使用另一个对象的属性字段来存取,每个方法都使用相同的计算,在循环中简单的加/乘运算。


public int accessSame(int loops)
{
m_value = 0;
for (int index = 0;
index

测试程序在一个大循环中反复的调用每个方法,在调用结束后计算平均时间。每个方法的第一次调用不包括在平均值中,因些初始化时间不是影响结果的因素。为这篇文章所做的测试运行,我为每个调用使用了10000000的循环计数,代码运行在1GHz PIII系统上。并且分别使用了三个不同的Linux JVM,对于每个JVM都使用了默认设置。

除了属性字段访问时间的测试以外,我对方法做了同样的测试。对于方法的调用,我偿试了与属性字段访问测试一样的三种方式,用额外使用了没有参数的方法的变量与传递并返回一个值的方法调用相对比。下面的代码显示了使用传递并返回值的调用方式进行测试的三种方法。


public int callDirectArgs(int loops)
{
int value = 0;
for (int index = 0;
index

虽然对于无参数的案例,执行效率从SUN1.3.1JVM的慢几百倍到IBM的JVM慢不到30倍,与属性字段访问案例相比,差别不是很大,这种情况的部分原因是因为java.lang.Integer的包装器需要传递和返回int类型的值。因为Intergers是不变的,因此就需要为每个方法的返回生成一个新值,这就增加了相当大的系统开销。

我还为使用反射创建对象编写了一个类似的效率测试程序。虽然这个例子与属性字段和方法调用相比差别不是很大,但是在Sun1.3.1JVM上调用newInstance()方法创建一个简单的java.lang.Object大约比直接使用new Object()方法长12倍的时间,在IBM1.4.0JVM上大约要长4倍的时间,在Sun1.4.1JVM上大约要长2倍的时间。对于任何用于测试的JVM,使用Array.newInstance(Type,size)方法创建一个数组所需要的时间比使用new tye[size]所花费的时间大约要长两倍,随着数组民尺寸的增长,这两种方法的差别的将随之减少。

Java 语言的反射机制提供了一种非常通用的动态连接程序组件的方法。它允许你的程序创建和维护任何类的对象(服从安全限制),而不需要提前对目标类进行硬编码。这些特征使得反射在创建与对象一同工作的类库中的通用方法方面非常有用。例如,反射经常被用于那些数据库,XML、或者其它的外部的持久化对象的框架中。

对于很多应用中的存在的缺点是使用反射可以使你的实际的代码内部逻辑变得模糊不清。程序员都希望在源代码中看到一个程序的逻辑以及象绕过源代码的反射所可能产生的维护问题这样的一些技术。
反射代码也比相应的直接代码要复杂一些,就像在性能比较的代码实例看到那样。处理这些问题的最好方法是尽可能少使用反射,只有在一些增加灵活性的地方来使用它。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值