这篇文章将介绍如何使用java reflection
api在运行时访问和使用一些相同的信息。为了帮助那些已经了解反射基础知识的开发人员保持有趣,我将介绍反射性能与直接访问的比较。
使用反射不同于普通的Java编程,因为它处理元数据——描述其他数据的数据。Java语言反射访问的特定类型的元数据是JVM中类和对象的描述。反射使您能够在运行时访问各种类信息。它甚至允许您读写字段和调用在运行时选择的类的方法。
反射是一个强大的工具。它允许您构建灵活的代码,这些代码可以在运行时组装,而不需要组件之间的源代码链接。但反射的某些方面可能存在问题。在本文中,我将探讨为什么您不希望在您的程序中使用反射,以及您为什么要这样做。在你了解了利弊之后,你可以自己决定什么时候利大于弊。
从 Class 开始
使用反射的起点始终是java.lang.Class
实例。如果您想使用一个预先确定的类,Java语言提供了一个简单的快捷方式来直接获取类实例:
Class clas = MyClass.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的反射
Class
对象为您提供了反射访问类元数据的所有基本钩子。此元数据包含有关类本身的信息,例如类的包和超类,以及类实现的接口。它还包括类定义的构造函数、字段和方法的详细信息。最后这些是编程中最常用的项目,因此我将在本节后面给出一些使用它们的示例。
对于这三种类型的类组件(构造函数、字段和方法)的java.lang.Class
提供四个独立的反射调用,以不同的方式访问信息。所有的要求都遵循标准。以下是用于查找构造函数的集合:
- 构造函数
getConstructor(Class[]params)
–使用指定的参数类型获取公共构造函数 - 构造函数[]
getConstructors()
——获取类的所有公共构造函数 - 构造函数
getDeclaredConstructor(Class[]params)
–使用指定的参数类型获取构造函数(无论访问级别如何) - 构造函数[]
getDeclaredConstructors()
——获取类的所有构造函数(无论访问级别如何)
每个调用都返回一个或多个java.lang.reflect
。构造函数实例。此构造函数类定义了一个newInstance
方法,该方法将对象数组作为其唯一参数,然后返回原始类的新构造实例。对象数组是用于构造函数调用的参数值。作为一个如何工作的示例,假设您有一个TwoString
类,该类具有一个接受一对字符串的构造函数,如清单1所示:
清单1。由一对字符串构造的类
public class TwoString {
private String m_s1, m_s2;
public TwoString(String s1, String s2) {
m_s1 = s1;
m_s2 = s2;
}
}
清单2中显示的代码获取构造函数并使用它创建一个使用字符串“a
”和“b
”的TwoString类的实例:
清单2。对构造函数的反射调用
Class[] types = new Class[] { String.class, String.class };
Constructor cons = TwoString.class.getConstructor(types);
Object[] args = new Object[] { "a", "b" };
TwoString ts = (TwoString)cons.newInstance(args);
清单2中的代码忽略了由各种反射方法引发的几种可能类型的已检查异常。异常在javadoc api描述中有详细说明,因此为了简洁起见,我将它们排除在所有代码示例之外。
在讨论构造函数的时候,Java编程语言还定义了一种特殊的快捷方法,可以用来创建一个没有参数(或默认)构造函数的类实例。快捷方式嵌入到类定义中,如下所示:
Object newInstance()
——使用默认构造函数构造新实例
尽管这种方法只允许您使用一个特定的构造函数,但如果您想要的话,它可以提供一个非常方便的快捷方式。当使用JavaBeans时,这种技术特别有用,JavaBeans是定义公共的、无参数构造函数所必需的。
关于class的反射还可以参考这篇文章:https://javakk.com/709.html
字段 Field 反射
访问字段信息的类反射调用与用于访问构造函数的类反射调用类似,使用字段名代替参数类型数组:
Field getField(String name)
–获取指定的公共字段
Field[] getFields()
——获取类的所有公共字段
字段getDeclaredField(字符串名称)
–获取类声明的命名字段
Field[] getDeclaredFields()
——获取类声明的所有字段
尽管与构造函数调用相似,但在字段方面有一个重要的区别:前两个变量返回可通过类访问的公共字段的信息,即使这些字段是从祖先类继承的。最后两个返回类直接声明的字段的信息——不管字段的访问类型如何。
这个java.lang.reflect
调用返回的.Field
实例为所有基元类型定义getXXX和setXXX方法,以及处理对象引用的通用get和set方法。您可以根据实际字段类型使用适当的方法,尽管getXXX方法将自动处理扩展转换(例如使用getInt方法检索字节值)。
清单3显示了一个使用字段反射方法的示例,该方法的形式是按名称递增对象的int字段:
清单3。通过反射增加
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;
}
这种方法开始显示出反射可能带来的一些灵活性。increment Field不使用特定的类,而是使用传入对象的getClass
方法来查找类信息,然后直接在该类中查找命名字段。
反射方法
对类的信息和访问方法的调用非常相似:
Method getMethod(String name,Class[]params)
–使用指定的参数类型获取命名的公共方法Method[] getMethods()
——获取类的所有公共方法Method getDeclaredMethod(String name,Class[]params)
–获取类使用指定参数类型声明的命名方法Method[] getDeclaredMethods()
——获取类声明的所有方法
与字段调用一样,前两个变量返回可通过类访问的公共方法的信息——甚至那些从祖先类继承的方法。最后两个返回类直接声明的方法的信息,而不考虑方法的访问类型。
这个java.lang.reflect
调用返回的方法实例定义了一个invoke
方法,可用于调用定义类的实例上的方法。这个invoke
方法接受两个参数,它们为调用提供类实例和参数值数组。
清单4进一步介绍了字段示例,显示了一个实际方法反射的示例。这个方法增加用get和set方法定义的int javabean属性。例如,如果对象为整型计数值定义了getCount
和setCount
方法,则可以在调用此方法时将“count”作为name参数传递,以增加该值。
清单4。通过反射递增JavaBean属性
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约定,我将属性名的第一个字母转换为大写,然后构造read方法名称并设置为构造write方法名。JavaBeans读取方法只返回值,写入方法将值作为唯一的参数,因此我指定要匹配的方法的参数类型。最后,约定要求方法是公共的,因此我使用查找的形式,查找类上可调用的公共方法。
这个例子是第一个使用反射传递基本值的例子,所以让我们看看它是如何工作的。基本原理很简单:每当您需要传递一个原语值时,只需替换相应包装类的实例(在java.lang
包)用于该类型的原语。这适用于调用和返回。所以,当我在示例中调用get方法时,我希望结果是java.lang.Integer
实际int属性值的包装器。
反射数组
数组是Java编程语言中的对象。像所有对象一样,它们也有类。如果您有一个数组,那么可以使用标准的getClass
方法获取该数组的类,就像对待任何其他对象一样。但是,在没有现有实例的情况下获取类的工作方式与其他类型的对象不同。即使你有了一个数组类,你也不能直接用它做什么——反射为普通类提供的构造函数访问对数组不起作用,数组也没有任何可访问的字段。只有基类java.lang.Object
方法是为数组对象定义的。
数组的特殊处理使用java.lang.reflect.Array
类。该类中的方法允许您创建新数组、获取数组对象的长度以及读取和写入数组对象的索引值。
清单5显示了一个有效调整现有数组大小的有用方法。它使用反射来创建一个相同类型的新数组,然后在返回新数组之前复制旧数组中的所有数据。
清单5。反射生长阵列
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编程语言定义了一种处理反射安全性的多级方法。基本模式是对反射实施与源代码访问相同的限制:
- 从任何地方访问类的公共组件
- 不能在类本身之外访问私有组件
- 对受保护和打包(默认访问)组件的有限访问
不过,有一个简单的方法可以绕过这些限制——至少有时候是这样。我在前面的示例中使用的构造函数、字段和方法类都扩展了一个公共基类–java.lang.reflect.AccessibleObject
类。此类定义了一个setAccessible
方法,该方法允许您打开或关闭这些类之一的实例的访问检查。唯一的问题是,如果存在安全管理器,它将检查关闭访问检查的代码是否具有这样做的权限。如果没有权限,安全管理器将抛出一个异常。
清单6演示了一个程序,该程序在清单1TwoString类的实例上使用反射来显示这一点:
清单6。反射安全措施
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)
调用。如果您取消field.setAccessible(true)
行,然后重新编译并重新运行代码,它将成功。最后,如果添加JVM参数-Djava.security.manager
在命令行上启用安全管理器,它将再次失败,除非您为ReflectSecurity类定义权限。
反射的性能
反射是一种强大的工具,但也有一些缺点。主要缺点之一是对性能的影响。使用反射基本上是一种解释操作,在这里,您告诉JVM您想做什么,它会为您完成。这种类型的操作总是比直接执行相同的操作慢。为了演示使用反射的性能成本,我为本文准备了一组基准测试程序。
清单7显示了字段访问性能测试的摘录,包括基本测试方法。每个方法都测试对字段的一种访问形式—accessSame
用于同一对象的成员字段,accessOther
使用直接访问的另一个对象的字段,accessReflection
使用通过反射访问的另一个对象的字段。在每种情况下,这些方法执行相同的计算——在循环中执行一个简单的加法/乘法序列。
清单7。现场访问性能测试代码
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 = (field.getInt(timing) +
ADDITIVE_VALUE) * MULTIPLIER_VALUE;
field.setInt(timing, value);
}
return timing.m_value;
} catch (Exception ex) {
System.out.println("Error using reflection");
throw ex;
}
}
测试程序以较大的循环计数重复调用每个方法,平均数次调用的时间测量值。第一次调用每个方法的时间不包括在平均值中,因此初始化时间不是结果的一个因素。在本文的测试运行中,我对每个调用使用了1000万的循环计数,运行在 1GHz 的 PIIIm 系统上。我对三个不同的 Linux JVM 的计时结果如图1所示。所有测试都使用每个 JVM的默认设置。
图1。现场访问时间
图表的对数刻度允许显示全部时间范围,但减少了差异的视觉影响。在前两组图形(sun jvm)的情况下,使用反射的执行时间比使用直接访问的执行时间大1000倍以上。相比之下,ibm jvm做 得更好,但是反射方法的时间仍然是其他方法的700倍以上。在任何JVM上,其他两种方法在时间上没有明显的差异,尽管 ibm jvm 的运行速度几乎是 sun jvm 的两倍。最有可能的是,这种差异反映了Sun HotSpot JVM 使用的专门化优化,这些优化在简单的基准测试中往往做得很差。
除了字段访问时间测试之外,我还为方法调用做了同样的计时测试。对于方法调用,我尝试了与字段访问相同的三种访问变体,增加了使用无参数方法与传递和返回方法调用值的变量。清单8显示了用于测试调用的传递值和返回值形式的三个方法的代码。
清单8。方法访问性能测试代码
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 = TimingClass.class.getMethod
("step", new Class [] { int.class });
Object[] args = new Object[1];
Object value = new Integer(0);
for (int index = 0; index < loops; index++) {
args[0] = value;
value = method.invoke(timing, args);
}
return ((Integer)value).intValue();
} catch (Exception ex) {
System.out.println("Error using reflection");
throw ex;
}
}
图2显示了方法调用的计时结果。在这里,反射比直接选择慢得多。不过,差异并不像字段访问情况那么大,从sun1.3.1 jvm上的速度慢几百倍到 ibm jvm 上无参数情况下慢不到30倍不等。带有参数的反射方法调用的测试性能比在所有jvm上没有参数的调用慢得多。这可能部分是因为java.lang.Integer
传递和返回的int值需要包装。因为整数是不可变的,所以需要为每个方法返回生成一个新的整数,这会增加相当大的开销。
图2。方法调用时间
在开发1.4 JVM 时,反射性能是Sun关注的一个方面,它显示在反射方法调用结果中。对于这种类型的操作,sun1.4.1 JVM 比1.3.1版本的 JVM 性能有了很大的提高,在我的测试中运行速度提高了大约7倍。IBM1.4.0 JVM再次为这个简单的测试提供了更好的性能,运行速度是SUN 1.4.1 JVM的两到三倍。
我还为使用反射创建对象编写了一个类似的计时测试程序。不过,这种情况下的差异并不像字段和方法调用那样显著。构造一个简单的java.lang.Object
使用newInstance()
调用的实例比在Sun 1.3.1 JVM上使用new Object()
要长大约12倍,在IBM 1.4.0 JVM上大约要长4倍,在Sun 1.4.1 JVM上只需要大约2倍的时间。使用构造数组Array.newInstance(type,size)
对于任何经过测试的JVM(类型,大小)使用new type[size]
的时间最多需要大约两倍的时间,并且随着数组大小的增加,差异会减小。
反射总结
Java语言反射提供了一种非常通用的动态链接程序组件的方法。它允许您的程序创建和操作任何类的对象(受安全限制),而无需提前对目标类进行硬编码。这些特性使得反射对于创建以非常通用的方式处理对象的库特别有用。例如,反射通常用于将对象持久化到数据库、XML或其他外部格式的框架中。
反射也有几个缺点。一是性能问题。当用于字段和方法访问时,反射比直接代码慢得多。这在多大程度上取决于反射在程序中的使用方式。如果它被用作程序操作中相对较少使用的一部分,那么慢性能就不会是一个问题。在我的测试中,即使是最坏情况下的计时数据也显示反射操作只需要几微秒。只有在性能关键型应用程序的核心逻辑中使用反射时,性能问题才会成为一个严重的问题。
对于许多应用程序来说,一个更严重的缺点是,使用反射会模糊代码中实际发生的事情。程序员希望在源代码中看到程序的逻辑,而绕过源代码的反射等技术可能会产生维护问题。反射代码也比相应的直接代码复杂,从性能比较的代码示例中可以看出。处理这些问题的最佳方法是谨慎地使用反射(只在它真正增加了有用的灵活性的地方)并在目标类中记录它的使用。