c反射机制介绍

    技术2024-03-24  9

    在“ Java编程动力学,第1部分 ”中,我向您介绍了Java编程类和类加载。 该文章描述了以Java二进制类格式提供的一些广泛的信息。 本月,我将介绍在运行时使用Java Reflection API访问和使用某些相同信息的基础知识。 为了使即使对于已经了解反射基础知识的开发人员来说,也使事情变得有趣,我将介绍反射性能与直接访问的比较。

    不要错过本系列的其余部分

    第1部分,“ 类和类加载 ”(2003年4月) 第3部分,“ 应用反射 ”(2003年7月) 第4部分,“ 使用Javassist进行类转换 ”(2003年9月) 第5部分,“ 即时转换类 ”(2004年2月) 第6部分,“ 使用Javassist进行面向方面的更改 ”(2004年3月) 第7部分,“ 使用BCEL进行字节码工程 ”(2004年4月) 第8部分,“ 用代码生成替换反射 ”(2004年6月)

    使用反射与普通Java编程的不同之处在于,它与元数据(描述其他数据的数据)一起使用。 通过Java语言反射访问的元数据的特定类型是JVM中的类和对象的描述。 反射使您可以在运行时访问各种类信息。 它甚至允许您读取和写入在运行时选择的类的字段和调用方法。

    反思是一个强大的工具。 它使您可以构建可在运行时进行汇编的灵活代码,而无需组件之间的源代码链接。 但是反思的某些方面可能会产生问题。 在这篇文章中,我将进入为什么你可能不希望在你的程序中使用反射的原因,以及该原因,你会的。 知道了取舍之后,您可以自己决定何时收益大于弊端。

    初级班

    使用反射的起点始终是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 case } // use the loaded class

    如果已经加载了该类,则将获取现有的Class信息。 如果尚未加载该类,则类加载器将立即加载它并返回新构造的类实例。

    对一堂课的思考

    Class对象为您提供了用于反射访问类元数据的所有基本钩子。 该元数据包括有关类本身的信息,例如类的包和超类,以及由该类实现的接口。 它还包括该类定义的构造函数,字段和方法的详细信息。 这些最后的项目是编程中最常用的项目,因此本节稍后将给出一些使用它们的示例。

    咨询专家:Dennis Sosnoski关于JVM和字节码的问题

    如果您对本系列文章中涉及的材料以及与Java字节码,Java二进制类格式或一般JVM问题有关的任何其他内容有任何意见或疑问,请访问由Dennis Sosnoski主持的JVM和Bytecode讨论论坛。

    对于这三种类型的类组件中的每一个-构造函数,字段和方法java.lang.Class提供了四个单独的反射调用,以不同的方式访问信息。 所有呼叫均遵循标准格式。 这是用于查找构造函数的集合:

    Constructor getConstructor(Class[] params) -使用指定的参数类型获取公共构造函数 Constructor[] getConstructors() -获取该类的所有公共构造函数 Constructor getDeclaredConstructor(Class[] params) -使用指定的参数类型获取构造函数(无论访问级别如何) Constructor[] getDeclaredConstructors() -获取该类的所有构造函数(无论访问级别如何)

    这些调用中的每一个都返回一个或多个java.lang.reflect.Constructor实例。 此Constructor类定义一个newInstance方法,该方法将对象数组作为唯一参数,然后返回原始类的新构造的实例。 对象数组是用于构造函数调用的参数值。 作为一个如何工作的示例,假设您有一个TwoString类,该类的构造函数采用一对String ,如清单1所示:

    清单1.由一对字符串构造的类
    public class TwoString { private String m_s1, m_s2; public TwoString(String s1, String s2) { m_s1 = s1; m_s2 = s2; } }

    清单2中所示的代码获取构造函数,并使用它使用String的"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编程语言还定义了一种特殊的快捷方式方法,您可以使用该方法创建具有无参数 (或默认)构造函数的类的实例。 快捷方式将嵌入到Class定义本身中,如下所示:

    Object newInstance() -使用默认构造函数构造新实例

    即使这种方法只允许您使用一个特定的构造函数,但如果您想要的话,它还是一个非常方便的快捷方式。 当使用JavaBeans来定义公共的无参数构造函数时,该技术特别有用。

    反射场

    对访问字段信息的Class反射调用类似于用于访问构造函数的调用,其中使用字段名称代替参数类型的数组:

    Field getField(String name) -获取命名的公共字段 Field[] getFields() -获取类的所有公共字段 Field getDeclaredField(String name) -获取由类声明的命名字段 Field[] getDeclaredFields() -获取该类声明的所有字段

    尽管与构造函数调用相似,但是在字段方面还是有一个重要的区别:前两个变体返回可通过类访问的公共字段的信息,甚至是从祖先类继承的信息。 最后两个返回有关类直接声明的字段的信息-与字段的访问类型无关。

    调用返回的java.lang.reflect.Field实例为所有原始类型定义getXXX和setXXX方法,以及与对象引用一起使用的通用get和set方法。 尽管getXXX方法将自动处理扩展转换(例如使用getInt方法检索byte值),但由您决定要根据实际的字段类型使用适当的方法。

    清单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; }

    这种方法开始显示一些反射可能的灵活性。 而不是使用特定的类, incrementField使用传入对象的getClass方法查找类信息,然后直接在该类中查找命名字段。

    反思方法

    访问方法信息的Class反射调用与构造函数和字段所使用的调用非常相似:

    Method getMethod(String name, Class[] params) -使用指定的参数类型获取命名的公共方法 Method[] getMethods() -获取该类的所有公共方法 Method getDeclaredMethod(String name, Class[] params) -使用指定的参数类型获取类声明的命名方法 Method[] getDeclaredMethods() -获取类声明的所有方法

    与字段调用一样,前两个变体返回可通过类访问的公共方法的信息-甚至是从祖先类继承的公共方法。 最后两个返回有关类直接声明的方法的信息,而与方法的访问类型无关。

    调用返回的java.lang.reflect.Method实例定义了一个invoke方法,您可以使用该方法在定义类的实例上调用该方法。 该invoke方法带有两个参数,这些参数提供了类实例和该调用的参数值数组。

    清单4使现场示例更进一步,显示了实际中方法反射的示例。 此方法增加使用get和set方法定义的int JavaBean属性。 例如,如果对象为整数count数值定义了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约定,我将属性名称的第一个字母转换为大写,然后在get以构造读取方法名称,并set为构造写入方法名称。 JavaBeans的读取方法仅返回值,而写入方法将值作为唯一参数,因此我为要匹配的方法指定参数类型。 最后,约定要求方法是公共的,因此我使用查找的形式来查找可在类上调用的公共方法。

    这个例子是我使用反射传递原始值的第一个例子,所以让我们看一下它是如何工作的。 基本原理很简单:只要需要传递原始值,就可以用相应包装器类的实例(在java.lang包中定义)替换该原始类型。 这适用于回叫。 因此,当我在示例中调用get方法时,我期望结果是实际int属性值的java.lang.Integer包装器。

    反射阵列

    数组是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编程语言定义了一种用于处理反射安全性的多级方法。 基本模式是对反射实施与对源代码访问相同的限制:

    从任何地方访问课程的公共组成部分 类之外无法访问私有组件 对受保护和程序包(默认访问)组件的访问受限

    解决这些限制的方法很简单-至少有时是这样。 我在前面的示例中使用的Constructor , Field和Method类都扩展了一个通用的基类java.lang.reflect.AccessibleObject类。 此类定义了setAccessible方法,该方法可让您打开或关闭这些类之一的实例的访问检查。 唯一的问题是,如果存在安全管理器,它将检查关闭访问检查的代码是否具有这样做的权限。 如果没有权限,安全管理器将引发异常。

    清单6演示了一个程序,该程序对清单1 TwoString类的实例使用了反射,以实际展示这一点:

    清单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)调用上引发IllegalAccessException 。 如果取消注释field.setAccessible(true)行,然后重新编译并重新运行代码,它将成功。 最后,如果您在命令行上添加JVM参数-Djava.security.manager以启用安全管理器,除非您为ReflectSecurity类定义权限,否则它将再次失败。

    反射表现

    反射是一种强大的工具,但存在一些缺点。 主要缺点之一是对性能的影响。 使用反射基本上是一种解释的操作,您可以在其中告诉JVM您想做什么并为您完成。 这种类型的操作总是比直接进行相同的操作要慢。 为了演示使用反射的性能成本,我为本文准备了一组基准测试程序(请参阅参考资料获得完整代码的链接)。

    清单7显示了现场访问性能测试的摘录,包括基本测试方法。 每种方法都测试一种对字段的访问方式accessSame与同一对象的成员字段accessOther使用, 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; } }

    测试程序会以较大的循环计数重复调用每种方法,以平均几次调用的时间测量结果。 平均值中不包括第一次调用每个方法的时间,因此初始化时间不是结果的因素。 在本文的测试运行中,我在一个1GHz PIIIm系统上运行的每个呼叫的循环计数为1000万。 我对三种不同的Linux JVM的计时结果如图1所示。所有测试均使用每个JVM的默认设置。

    图1.字段访问时间

    图表的对数刻度可以显示整个时间范围,但可以减少差异的视觉影响。 对于前两组图形(Sun JVM),使用反射的执行时间比使用直接访问的执行时间长1000倍以上。 与之相比,IBM JVM的性能要好一些,但是反射方法仍然是其他方法的700倍以上。 尽管JVM的运行速度几乎是Sun JVM的两倍,但是在任何JVM上,其他两种方法之间的时间没有显着差异。 这种差异很可能反映了Sun Hot Spot 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显示了我的方法调用计时结果。 同样,反射比直接替代要慢得多。 但是,差异没有现场访问情况那么大,在无参数情况下,范围从Sun 1.3.1 JVM的速度慢了几百倍,到IBM JVM的速度慢了不到30倍。 在所有JVM上,带参数的反射方法调用的测试性能比没有参数的调用的性能要慢得多。 这可能部分是由于传递和返回int值所需的java.lang.Integer包装器。 由于Integer是不可变的,因此需要为每个方法返回值生成一个新值,从而增加了可观的开销。

    图2.方法调用时间

    在开发1.4 JVM时,反射性能是Sun重点关注的领域之一,这在反射方法调用结果中得以体现。 对于这种类型的操作,Sun 1.4.1 JVM显示出比1.3.1版本大大提高的性能,在我的测试中,运行速度提高了大约7倍。 但是,IBM 1.4.0 JVM在此简单测试中再次提供了更好的性能,运行速度比Sun 1.4.1 JVM高出两到三倍。

    我还编写了类似的时序测试程序,用于使用反射创建对象。 但是,这种情况下的差异几乎不像字段和方法调用情形那样重要。 使用newInstance()调用构造一个简单的java.lang.Object实例所需的时间比在Sun 1.3.1 JVM上使用new Object()时间长约12倍,在IBM 1.4.0 JVM上的时间长约4倍,而大约只有2倍。在Sun 1.4.1 JVM上运行时间增加了两倍。 对于任何经过测试的JVM,使用Array.newInstance(type, size)构造数组所花费的时间最多比使用new type[size]大约长两倍,并且随着数组大小的增加,差异会减小。

    反思总结

    Java语言反射提供了一种动态链接程序组件的非常通用的方法。 它使您的程序可以创建和操作任何类的对象(受安全性限制),而无需提前对目标类进行硬编码。 这些功能使反射对于创建以非常通用的方式处理对象的库特别有用。 例如,反射常用于将对象持久保存到数据库,XML或其他外部格式的框架中。

    反思也有两个缺点。 一是性能问题。 当用于字段和方法访问时,反射比直接代码要慢得多。 重要的程度取决于程序中反射的使用方式。 如果将它用作程序操作中相对少见的部分,则不会担心性能下降。 在我的测试中,即使是最坏情况下的时序图,也表明反射操作仅需几微秒。 如果在性能关键型应用程序的核心逻辑中使用反射,则仅会严重关注性能问题。

    对于许多应用程序而言,更严重的缺点是使用反射会掩盖代码内部的实际情况。 程序员希望在源代码中看到程序的逻辑,而绕过源代码的诸如反射之类的技术可能会造成维护问题。 从性能比较的代码样本中可以看出,反射代码也比相应的直接代码复杂。 解决这些问题的最佳方法是,仅在确实增加有用的灵活性的地方谨慎使用反射,并在目标类中记录其使用。

    在下一部分中,我将提供有关如何使用反射的更详细的示例。 此示例提供了一个API,用于处理Java应用程序的命令行参数,您可能会发现该工具对自己的应用程序很有用。 它也建立在反思的优势上,同时避免了劣势。 反射可以简化您的命令行处理吗? 在Java编程动力学的第3部分中找到。


    翻译自: https://www.ibm.com/developerworks/java/library/j-dyn0603/index.html

    相关资源:反射机制详解
    Processed: 0.014, SQL: 8