Java反射实体类属性(get,t⽅法)
反射授予了你的代码访问装载进JVM内的Java类的内部信息的权限,并且允许你编写在程序执⾏期间与所选择的类的⼀同⼯作的代码,⽽不是在源代码中。这种机制使得反射成为创建灵活的应⽤程序的强⼤⼯具,但是要⼩⼼的是,如果使⽤不恰当,反射会带来很⼤的副作⽤。在这篇⽂章中,咨询顾问Dennis Sosnoski 介绍了反射的使⽤,同时还介绍了⼀些使⽤反射所要付出的代价。在这⾥,你可以找到Java反射API是如何在运⾏时让你钩⼊对象的。
在第⼀部分,我向你介绍了Java程序设计的类以及类的装载。那篇⽂章中描述了很多出现在Java⼆进制类格式中的信息,现在我来介绍在运⾏时使⽤反射API访问和使⽤这些信息的基础。为了使那些已经了解反射基础的开发⼈员对这些事情感兴趣,我还会介绍⼀些反射与直接访问的在性能⽅⾯的⽐较。
使⽤反射与和metadata(描述其它数据的数据)⼀些⼯作的Java程序设计是不同的。通过Java语⾔反射来访问的元数据的特殊类型是在JVM内部的类和对象的描述。反射使你可以在运⾏时访问各种类信息,它甚⾄可以你让在运⾏时读写属性字段、调⽤所选择的类的⽅法。
反射是⼀个强⼤的⼯具,它让你建⽴灵活能够在运⾏时组装的代码,⽽不需要连接组件间的源代码。反射的⼀些特征也带来⼀些问题。在这章中,我将会探究在应⽤程序中不打算使⽤反射的原因,以为什么使⽤它的原因。在你了解到这些利弊之后,你就会在好处⼤于缺点的时候做出决定。
初识class
使⽤反射的起点总时⼀个java.lang.Class类的实例。如果你与⼀个预先确定的类⼀同⼯作,Java语⾔为直接获得Class类的实例提供了⼀个简单的快捷⽅式。例如:
Class clas = MyClass.class;
当你使⽤这项技术的时候,所有与装载类有关的⼯作都发⽣在幕后。如果你需要在运⾏时从外部的资源中读取类名,使⽤上⾯这种⽅法是不会达到⽬的的,相反你需要使⽤类装载器来查找类的信息,⽅法如下所⽰:
// "name" is the class name to load Class clas = null; try { clas = Class.forName(name); } catch (ClassNotFoundException ex) { // handle exception ca } // u the loaded class
如果类已经装载,你将会找到当前在在的类的信息。如果类还没有被装载,那么类装载器将会装载它,并且返回最近创建的类的实例。
关于类的反射
Class对象给予你了所有的⽤于反射访问类的元数据的基本钩⼦。这些元数据包括有关类的⾃⾝信息,
例如象类的包和⼦类,还有这个类所实现的接⼝,还包括这个类所定义的构造器、属性字段以及⽅法的详细信息。后⾯的这些项是我们在程序设计过种经常使⽤的,因此在这⼀节的后⾯我会给出⼀些⽤这些信息来⼯作的例⼦。
对于类的构造中的每⼀种类型(构造器、属性字段、⽅法),java.lang.Class提供了四种独⽴的反射调⽤以不的⽅式来访问类的信息。下⾯列出了这四种调⽤的标准形式,它是⼀组⽤于查找构造器的调⽤。
Constructor getConstructor(Class[] params) 使⽤指定的参数类型来获得公共的构造器;
Constructor[] getConstructors() 获得这个类的所有构造器;
Constructor getDeclaredConstructor(Class[] params) 使⽤指定的参数类型来获得构造器(忽略访问的级别)
Constructor[] getDeclaredConstructors() 获得这个类的所有的构造器(忽略访问的级别)
上述的每⼀种⽅法都返回⼀或多个flect.Constructor的实例。Constructor类定义了⼀个需要⼀个对象数据做为唯⼀参数的newInstance⽅法,然后返回⼀个最近创建的原始类的实例。对象数组是在构造器调⽤时所使⽤的参数值。例如,假设你有⼀个带有⼀对String 类型做为参数的构造器的T
woString类,代码如下所⽰:
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 = Constructor(types); Object[] args = new Object[] { "a", "b" }; TwoString ts = wInstance(args);
上⾯的代码忽略了⼏种可能的被不同的反射⽅法抛出的异常检查的类型。这些异常在JavadocAPI中有详细的描述,因此为简便起见,我会在所有的代码中忽略它们。
在我涉及到构造器这个主题时,Java语⾔也定义了⼀个特殊的没有参数的(或默认)构造器快捷⽅法,你能使⽤它来创建⼀个类的实例。这个快捷⽅法象下⾯的代码这样被嵌⼊到类的⾃定义中:
Object newInstance() ?使⽤默认的构造器创建新的实例。敬老院送温暖活动
尽管这种⽅法只让你使⽤⼀个特殊的构造器,但是如果你需要的话,它是⾮常便利的快捷⽅式。这项
技术在使⽤JavaBeans⼯作的时候尤其有⽤,因为JavaBeans需要定义⼀个公共的、没有参数的构造器。
通过反射来查找属性字段
Class类反射调⽤访问属性字段信息与那些⽤于访问构造器的⽅法类似,在有数组类型的参数的使⽤属性字段名来替代:使⽤⽅法如下所⽰:
Field getField(String name) --获得由name指定的具有public级别的属性字段
Field getFields() ?获得⼀个类的所有具有public级别的属性字段
Field getDeclaredField(String name) ?获得由name指定的被类声明的属性字段
Field getDeclaredFields() ?获得由类定义的所有的属性字段
游湖南尽管与构造器的调⽤很相似,但是在提到属性字段的时候,有⼀个重要的差别:前两个⽅法返回能过类来访问的公共(public)属性字段的信息(包括那些来⾃于超类的属性字段),后两个⽅法返回由类直接声明的所有的属性字段(忽略了属性字段的访问类型)。
flect.Field的实例通过调⽤定义好的getXXX和tXXX⽅法来返回所有的原始的数据类型,就像普通的与对象引⽤⼀起⼯作的get和t⽅法⼀样。尽管getXXX⽅法会⾃动地处理数据类型转换(例如使⽤getInt⽅法来获取⼀个byte类型的值),但使⽤⼀个适当基于实际的属性字段类型的⽅法是应该优先考虑的。
下⾯的代码显⽰了如何使⽤属性字段的反射⽅法,通过指定属性字段名,找到⼀个对象的int类型的属性字段,并给这个属性字段值加1。
public int incrementField(String name, Object obj) { Field field = Class().getDeclaredField(name); int value = Int(obj) + 1; field.tInt(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类型的⽅法?包括那些继承于超类的⽅法。后两个⽅法返回由这个类直接声明的⽅法的信息,⽽不管⽅法的访问类型。
通过调⽤返回的flect.Mehtod实例定义了⼀个invoke⽅法,你可以使⽤它来调⽤定义类的有关实例。这个invoke⽅法需要两个参数,⼀个是提供这个⽅法的类的实例,⼀个是调⽤这个⽅法所需要的参数值的数组。
下⾯给出了⽐属性字段的例⼦更加深⼊的例⼦,它显⽰了⼀个的⽅法反射的例⼦,这个⽅法使⽤get和t⽅法来给JavaBean定义的int类型的属性做增量操作。例如,如果对象为⼀个整数类型count属性定义了getCount和tCount⽅法,那么为了给这个属性做增量运算,你就可以把“count”做为参数名传递
给调⽤的这个⽅法中。⽰例代码如下:
public int incrementProperty(String name, Object obj) { String prop = UpperCa(name.charAt(0)) + name.substring(1); String mname = "get" + prop; Class[] types = new Class[] {}; Method method = Class().getMethod(mname, types); Object result = method.invoke(obj, new Object[0]); int value = ((Integer)result).intValue() + 1; mname = "t" + prop; types = new Class[] { int.class }; method = Class().getMethod(mname, types); method.invoke(obj, new Object[] { new Integer(value) }); return value; }
根据JavaBeans的规范,我把属性名的第⼀个字母转换为⼤写,然后在前⾯加上“get”来建⽴读取属性值的⽅法名,在属性名前加
上“t”来建⽴设置属性值的⽅法名。JavaBeans的读⽅法只返回属性值,写⽅法只需要要写⼊的值做为参数,因此我指定了与这个⽅法相匹配的参数类型。最后规范规定这两个⽅法应该是public类型的,因此我使⽤了查找相关类的public类型⽅法的调⽤形式。
多肉的何老师
这个例⼦我⾸先使⽤反射传递⼀个原始类型的值,因此让我们来看⼀下它是怎样⼯作的。基本的原理是简单的:⽆论什么时候,你需要传递⼀个原始类型的值,你只要替换相应的封装原始类型的(在java.lang 包中定义的)的类的实例就可以了。这种⽅法可应⽤于调⽤和返回。因此在我的例⼦中调⽤
get⽅法时,我预期的结果是⼀个由java.lang.Integer类所封装的实际的int类型的属性值。
夏仁虎
反射数组
在Java语⾔中数组是对象,象其它所有的对象⼀样,它有⼀些类。如果你有⼀个数组,你可以和其它任何对象⼀样使⽤标准的getClass⽅法来获得这个数组的类,但是你获得的这个类与其它的对象类型相⽐,不同之处在它没有⼀个现存的⼯作实例。即使你有了⼀个数组类之后,你也不能够直接⽤它来做任何事情,因为通过反射为普通的类所提供的构造器访问不能为数组⼯作,并且数组没有任何可访问的属性字段,只有基本的为数组对象定义的java.lang.Object类型的⽅法。
数组特殊处理要使⽤flect.Array类提供的⼀个静态⽅法的集合,这个类中的⽅法可以让你创建新的数组,获得⼀个数组对象的长度,以及读写⼀个数组对象的索引值。
下⾯的代码显⽰了有效调整⼀个现存数组的尺⼨的⽅法。它使⽤反射来创建⼀个相同类型的新数组,然后在返回这个新数组之前把原数组中的所有的数据复制到新的数组中。
public Object growArray(Object array, int size) { Class type = Class().getComponentType(); Object grown = wInstance(type, size);
System.arraycopy(array, 0, grown, 0, Math.Length(array), size)); return grown; }
与反射
在处理反射的时候,安全是⼀个复杂的问题。反射正常被框架类型的代码使⽤,并因为这样,你可能会经常要求框架不关⼼普通的访问限制来完全访问你的代码。然⽽,⾃由的访问可能会在其它的⼀些实例中产⽣⼀些风险,例如在代码在⼀个不被信任的代码共享环境中被执⾏的时候。
因为这些冲突的需要,Java语⾔定义了⼀个多级⽅法来处理反射安全。基本的模式是在反射请求源码访问的时候强制使⽤如下相同的约束限制:
访问这个类中来⾃任何地⽅的public组件;
5月活动不访问这个类本⾝外部的private组件;
限制访问protected和package(默认访问)组件。
围绕这些限制有⼀个简单的⽅法,我在前⾯的例⼦中所使⽤的所有构造器、属性字段、以及类的⽅法都扩展于⼀个共同的基类flect.AccessibleObject类。这个类定义了⼀个tAccessible⽅法,这个⽅法可以让你打开或关闭这些对类的实例的访问检查。如果安全管理器被设置为关闭访问检查,那么就允许你访问,否则不允许,安全管理器会抛出⼀个异常。
下⾯是⼀个使⽤反向来演⽰这种⾏为的TwoString类的实例。
public class ReflectSecurity { public static void main(String[] args) { try { TwoString ts = new TwoString("a", "b"); Field field = DeclaredField("m_s1"); // field.tAccessible(true); System.out.println("Retrieved value is " + (inst)); } catch (Exception ex) { ex.printStackTrace(System.out); } } }
如果你编译这段代码并且直接使⽤不带任何参数的命令⾏命令来运⾏这个程序,它会抛出⼀个关于(inst)调⽤的IllegalAccessException异常,如果你去掉上⾯代码中field.tAccessible(true)⾏的注释,然后编译并重新运⾏代码,它就会成功执⾏。最后,如果你在命令⾏给JVM添加⼀个Djava.curity.manager参数,使得安全管理器可⽤,那么它⼜会失败,除⾮你为ReflectSecurity 类定义安全许可。
反射性能
反射是⼀个强⼤的⼯具,但是也会带⼀些缺点。主要缺点之⼀就是对性能的影响。使⽤反射是基本的解释性操作,你告诉JVM你要做什么,它就会为你做什么。这种操作类型总是⽐直接做同样的操作要慢。为了演⽰使⽤反射所要付出的性能代价,我为这篇⽂章准备了⼀套基准程序(可以从资源中下载)。
下⾯列出⼀段来⾃于属性字段的访问性能测试的摘要,它包括基本的测试⽅法。每个⽅法测试⼀种访
问属性字段的形式,accessSame⽅法和本对象的成员字段⼀起⼯
作,accessReference⽅法直接使⽤另外的对象属性字段来存取,accessReflection通过反射使⽤另⼀个对象的属性字段来存取,每个⽅法都使⽤相同的计算在循环中简单的加/乘运算。
public int accessSame(int loops) { m_value = 0; for (int index = 0; index < loops; index++) { m_value = (m_value + ADDITIVE_VALUE) * MULTIPLIER_VALUE; } return m_value; } public int accessReference(int loops) { TimingClass timing = new TimingClass(); for (int index = 0; index < loops; index++) { timing.m_value = (timing.m_value + ADDITIVE_VALUE) * MULTIPLIER_VALUE; } return timing.m_value; } public int accessReflection(int loops) throws Exception { TimingClass timing = new TimingClass(); try { Field field = TimingClass.class. getDeclaredField("m_value"); for (int index = 0; index < loops; index++) { int value = (Int(timing) + ADDITIVE_VALUE) * MULTIPLIER_VALUE; field.tInt(timing, value); } return timing.m_value; } catch (Exception ex) { System.out.println("Error using reflection"); throw ex; } }
赞美妈妈测试程序在⼀个⼤循环中反复的调⽤每个⽅法,在调⽤结束后计算平均时间。每个⽅法的第⼀次调⽤不包括在平均值中,因些初始化时间不是影响结果的因素。为这篇⽂章所做的测试运⾏,我为每个调⽤使⽤了10000000的循环计数,代码运⾏在1GHzPIII系统上。并且分别使⽤了三个不同的LinuxJVM,对于每个JVM都使⽤了默认设置,测试结果如下图所⽰:
上⾯的图表的刻度可以显⽰整个测试范围,但是那样的话就会减少差别的显⽰效果。这个图表中的前两个是⽤SUN的JVM的进⾏测试的结果图,使⽤反射的执⾏时间⽐使⽤直接访问的时间要超过1000多倍。最后⼀个图是⽤IBM的JVM所做的测试,通过⽐较要SUN的JVM执⾏效率要⾼⼀些,但是使⽤反射的⽅法依然要⽐其它⽅法超出700多倍。虽然IBM的JVM要⽐SUN的JVM⼏乎要快两倍,但是在使⽤反射之外的两种⽅法之间,对于任何的JVM在执⾏效率上没有太⼤的差别。最⼤的可能是,这种差别反映了通过Sun Hot Spot JVMs在简化基准⽅⾯所做的专门优化很少。
除了属性字段访问时间的测试以外,我对⽅法做了同样的测试。对于⽅法的调⽤,我偿试了与属性字段访问测试⼀样的三种⽅式,⽤额外使⽤了没有参数的⽅法的变量与传递并返回⼀个值的⽅法调⽤相对⽐。下⾯的代码显⽰了使⽤传递并返回值的调⽤⽅式进⾏测试的三种⽅法。
public int callDirectArgs(int loops) { int value = 0; for (int index = 0; index < loops; index++) { value = step(value); } return value; } public int callReferenceArgs(int loops) { TimingClass timing = new TimingClass(); int value = 0; for (int index = 0; index < loops; index++) { value = timing.step(value); } return value; } public int callReflectArgs(int loops) throws Exception { TimingClass timing = new TimingClass(); try { Method method = Method ("step", new Class [] {
馍馍int.class }); Object[] args = new Object[1]; Object value = new Integer(0); for (int index = 0; index < lo
ops; index++) { args[0] = value; value = method.invoke(timing, args); } return ((Integer)value).intValue(); } catch (Exception ex) { System.out.println("Error using reflection"); throw ex; } }
下图显⽰我使⽤这些⽅法的测试结果,这⾥再⼀次显⽰了反射要⽐其它的直接访问要慢很多。虽然对于⽆参数的案例,执⾏效率从SUN1.3.1JVM的慢⼏百倍到IBM的JVM慢不到30倍,与属性字段访问案例相⽐,差别不是很⼤,这种情况的部分原因是因为java.lang.Integer的包装器需要传递和返回int类型的值。因为Intergers是不变的,因此就需要为每个⽅法的返回⽣成⼀个新值,这就增加了相当⼤的系统开销。
反射的性能是SUN在开发1.4JVM时重点关注的⼀个领域,从上图可以看到改善的结果。Sun1.4.1JVM对于这种类型的操作⽐1.3.1版有了很⼤的提⾼,要我的测试中要快⼤约7倍。IBM的1.4.0JVM对于这种测试提供了更好的性能,它的运⾏效率要⽐Sun1.4.1JVM快两到三倍。
我还为使⽤反射创建对象编写了⼀个类似的效率测试程序。虽然这个例⼦与属性字段和⽅法调⽤相⽐差别不是很⼤,但是在Sun1.3.1JVM上调⽤newInstance()⽅法创建⼀个简单的java.lang.Object⼤约⽐直接使⽤new Object()⽅法长12倍的时间,在IBM1.4.0JVM上⼤约要长4倍的时间,在Sun1.4.1JVM上⼤约要长2倍的时间。对于任何⽤于测试的JVM,使⽤wInstance(Type,size)
⽅法创建⼀个数组所需要的时间⽐使⽤new tye[size]所花费的时间⼤约要长两倍,随着数组民尺⼨的增长,这两种⽅法的差别的将随之减少。
反射概要总结
Java 语⾔的反射机制提供了⼀种⾮常通⽤的动态连接程序组件的⽅法。它允许你的程序创建和维护任何类的对象(服从安全限制),⽽不需要提前对⽬标类进⾏硬编码。这些特征使得反射在创建与对象⼀同⼯作的类库中的通⽤⽅法⽅⾯⾮常有⽤。例如,反射经常被⽤于那些数据库,XML、或者其它的外部的持久化对象的框架中。
反射还有两个缺点,⼀个是性能问题。在使⽤属性字段和⽅法访问的时候,反射要⽐直接的代码访问要慢很多。⾄于对影响的程度,依赖于在程序中怎样使⽤反射。如果它被⽤作⼀个相关的很少发⽣的程序操作中,那么就不必关⼼降低的性能,即使在我的测试中所展⽰的最耗时的反射操作的图形中也只是⼏微秒的时间。如果要在执⾏应⽤程序的核⼼逻辑中使⽤反射,性能问题才成为⼀个要严肃对象的问题。
对于很多应⽤中的存在的缺点是使⽤反射可以使你的实际的代码内部逻辑变得模糊不清。程序员都希望在源代码中看到⼀个程序的逻辑以及象绕过源代码的反射所可能产⽣的维护问题这样的⼀些技术。反射代码也⽐相应的直接代码要复杂⼀些,就像在性能⽐较的代码实例看到那样。处理这些问题的最
好⽅法是尽可能少使⽤反射,只有在⼀些增加灵活性的地⽅来使⽤它。
在下⼀篇⽂章中,我将给出⼀个更加详细的如何使⽤反射的例⼦。这个例⼦提供了⼀个⽤于处理传递给⼀个Java应⽤程序的命令⾏参数的API。在避免弱点的同时,它也显⽰了反射的强⼤的功能,反射能够使⽤的你的命令处理变得的简单吗?你可以在Java 动态程序设计的第三部分中找到答案。