Java动态绑定的方法重载的实现
2015-03-15严忠林
严忠林
Java动态绑定的方法重载的实现
严忠林
摘要:Java支持“方法重载”,但其执行代码是在编译时就确定的,不能根据运行时的实际对象动态改变,这有时会增加代码的复杂性。通过使用JSR-292提供的功能,可以实现一个框架,让Java拥有在运行时绑定重载代码的能力。可以提高程序的简明性、可重用性和可扩展性。
关键词:方法重载;方法重写;动态调用指令;方法句柄;Java类文件处理
0 引言
作为面向对象语言,Java支持“方法重写(Method Overriding)”。针对同一方法签名,父类和各个子类可以提供不同的实现代码,运行时将根据实际对象的不同自动选择。它是实现多态性的基础,使同样的代码,在不同的对象上运行,即可获得不同的结果。通过添加新的子类,不改变已有代码,就能扩展系统功能,大大提高了软件开发的效率和质量。这是面向对象方法实现代码可重用、可扩展的基本手段。
在语法上,Java也支持“方法重载(Method Overloading)”。在一个类中,可以为同一方法名但不同的参数编写不同的方法体,程序能依据调用时参数的类型、数量、次序,选择相应的代码执行。从概念上可以认为这些方法体进行的是同一处理,但却可根据外界提供的不同条件“随机应变”。这对提高程序的易读易写性很有帮助。
1 方法重写与方法重载
方法重号方法重载两种方式都提高了编程的方便、灵活性,但它们的执行机制是完全不同的[1]。对于所执行方法体的确定时机,重写采用的是“动态绑定”或叫“晚捆绑”。它是直到运行时,才依据方法栈中首个参数引用的对象,来选择应该执行的代码(在Java源代码中无此参数,但编译后的bytecode会将消息的接受者作为首个参数送入方法栈)。而重载则是“静态绑定”,所执行方法体是在编译时根据参数类型、数量等不同确定的,运行时没有选择,直接运行。对于方法体的选择依据,两者也不同。重写采用的是“单分派”,只根据栈帧中首个参数的不同来做选择。而重载是“多分派”,能综合所有参数的情况加以选择。
两种方式各有特点。重载高效,虽然要参照多个参数,但所有工作都在编译时完成,运行时无额外开销。但由于无法利用运行时信息,操作完全固定,所以灵活性不够。而重写正好相反,运行时需进行选择,有一定性能损失。但相同的代码,却能依据运行时情况,执行不同的处理。
目前Java等语言对重写方式都选择单分派进行。既能提供灵活性,运行时又只需最简单的处理(一般是通过查找存储于类中的虚方法表来获得方法入口地址),可最大限度地保证运行效率。综合各方面考虑,这确实是最佳选择。但在某些特定情况下,单分派提供的灵活性仍然不够,有时也会增加程序的复杂性,不利于程序的扩展。
举个简单例子,假如要处理几何问题,定义了采用直角坐标表示的点(Class Point)和线(Class Line)。Line类中用k、b字段记录直线的斜率和截距,许多运算都要使用它们。但对平行于y轴的直线,k、b在运算时无意义,需要再定义一个子类VLine,添加字段a记录x轴的截距。它的许多处理和普通直线运算不同,因此需要重写这些方法。
现在要求两条直线的交点,针对不同类型的直线有不同的计算方法,在Line和VLine两个类中都要分别重载实现它们,假如是crossPoint(Line l)和crossPoint(VLine l)。现在main()中有两个直线数组,用双重循环重复执行语句①,应该能求出它们所有的交点,如图1所示:
但由于重载的静态限制,编译时就根据声明绑定了Line型参数的方法,而运行时l2却可以是VLine对象,所以语句①并不正确。
这类问题的传统解决方案是在代码中添加if语句,对参数类型加以判断,再选择执行不同的处理。然而这已经失去了面向对象的意趣。对于这儿的简单情况,此方案当然无伤大雅。但假如系统的继承关系、要进行的处理都比较复杂,那么繁复的if结构将使代码变得庞大臃肿,难读难改,成为程序员的负担。
更重要的是这种处理方式不具有可扩展性。像上述的几何系统,假如在完成以后,又需要引入极坐标表示法和相应处理。这时仅仅添加新的点、线子类是不够的,仍然不得不打开所有已完成的源代码,在众多类中,毫无遗漏地找出相关if语句,正确地进行修改添加。对于一个复杂系统,这决不是一件轻松的工作。
图1 示例代码
实际上,这类问题的最佳解决方案是“多分派+动态绑定”,即使用直到运行时才进行绑定的方法重载机制。如果有这样的机制,系统就会根据运行时各参数的实际情况,自动选择最合适的方法执行,代码中不再需要类型判定,添加新的子类也因此变得简单。然而,方法何时绑定、如何绑定,是编程语言的内部机制,是由语言的设计者决定的。在过去,语言使用者是完全无能为力的。不过,自JDK1.7起,实现了动态类型语言支持(JSR-292),这方面情况已有所改观。
2 JVM的动态类型语言支持
JSR-292[2]的最初出发点是为了便于在JVM虚拟机上,高效实现类似于JRuby、Jython、Groovy、Clojure这样的动态类型语言。动态类型语言不需要变量声明,变量类型在运行时确定,并据此进行不同的处理。例如对于表达式a+b,要等到运行到此处时,才会根据a、b类型确定是做整数加、浮点数加、还是字符串加。这类语言简洁、灵活,近来越来越受欢迎,但JVM对它们的支持一直有所欠缺。
JVM的bytecode原有4条方法调用指令[3],invokevirtual、invokespecial、invokeinterface和invokestatic,分别是调用动态绑定的成员方法、不需动态绑定的成员方法、接口定义的方法和类方法。它们最终执行的方法体都是由虚拟机根据编译时嵌入指令中的符号引用,按照固定的查找规则确定的。它们适用于Java语言,但其他语言要按照自己的规则动态确定执行的方法体,就不那么容易了,需要使用种种特异手法去迁就JVM中的这些规则。这势必增加语言实现的复杂度,也带来额外的性能和内存开销。因此JSR-292引入了一条新的调用指令invokedynamic,允许其他语言实现者按照自己的调用规则,确定最终执行的代码,从而直接在虚拟机层面上解决了此问题。
为新增加的invokedynamic指令,JVM在类文件中还添加了一些描述它的新的常量、属性元素。又在API中引入了java.lang.invoke包[4],提供支持这些操作的相关类。其中最重要的是方法句柄类(MethodHandle),它提供在虚拟机底层访问类或对象中各成员的机制,无论是方法调用还是数据访问,都直接加以处理,甚至连其他调用指令都要执行的权限校验等行为也会跳过。对于现今日益发展的JIT、Hotspot等技术带来的丰富的优化措施,它们也能够享用。因而用它们进行处理,能保持代码执行的高效率。MethodHandle对象是强类型的,由MethodType描述它的参数和返回值类型。使用时,提供的参数应满足其类型要求,当然它也有能进行各种类型转换的方法,用户可根据需要选用。java.lang.invoke包中还提供了许多生成、变换、组合MethodHandle的方法,使用者可根据自己实现的规则的要求,在使用前后作各种处理,达成希望的结果。
该包中还有CallSite类,每个invokedynamic指令中都含有此对象的引用,它记录着虚拟机运行至此处时要执行的方法句柄。初次运行时,该引用是空的,虚拟机要执行程序中先前嵌入的自举方法句柄BootStrapMethodHandle,根据运行时信息,生成相应对象,确定以后将执行的动作。
3 动态方法重载的实现
利用这些类和对象,可以为虚拟机制定动态调用的各种规则。利用它们,也可实现动态绑定的方法重载。具体思路是在执行这些方法调用时,首先,查看运行栈中各参数的实际类型,依据这些信息,查找到最匹配的方法体,再转入执行。为了和Java原来的调用机制一致,匹配时以排在前面的参数优先为原则,即先找第一个参数的最匹配类,再找第二个,以此类推。为尽量减少对代码执行效率造成的影响,这种搜寻工作不宜在运行时进行。因此,要事先将参数类型的各种可能组合与方法句柄的对应放入一个HashMap,到运行时只要用实际类型做一次散列映射,就能获得最匹配的方法。这样就实现了基于多分派的运行时进行的方法捆绑,即动态的方法重载。如在此机制下执行图1中的求两线交点的语句①,不管l1、l2为何类型,都能得到正确结果。
这种基于栈中参数类型进行的映射,还可以处理得更灵活,使通过添加子类来扩展系统的工作更简单方便。仍以求两线交点为例,假如在直角坐标系统完成以后,要添加对极坐标的支持,当然要添加新的点、线子类(class P_Point和class P_Line)。为使语句①依然在任何情况下都能正确运行,除了在新的P_Line类中需有重载各个crossPoint方法以外,在旧的Line和VLine类中仍要添加新的Point crossPoint(P_Line l)方法,是否能不修改已有代码呢?
现在完全可以通过在新类中定义静态方法来替代,如图2所示:
图2 新添加的子类
定义static Point crossPoint(Line la, P_Line lb)方法,将所有对((Line)l1).crosspoint((P_Line)l2)的调用映射为对此方法的调用。这两种方法调用,方法栈中的参数结构完全一样。本来因为捆绑机制不同,需用不同的调用指令,所以不能混用。但使用这里自定义的映射方案,却可以同样处理。这样所有的扩展工作就只出现在新建的子类中,在新类中为旧的类添加它所缺少的方法,旧的代码可以完全保持不变。这对于新系统的实现和维护,无疑是有好处的。
JSR-292是专为支持新的动态语言设计的,没准备用于Java,所以在高级语言层面,通过Java源代码不能直接生成invokedynamic指令和相应结构。所有工作必须在bytecode层面,通过修改编译生成的类文件进行。好在JVM的类文件有严格定义的结构,所有的类、变量、方法,包括参数信息等都通过字符串构成的明晰符号来说明和引用[3]。只要熟悉这些结构和bytecode指令,通过添加、修改相应元素,依然可以达成需要功能。
为了明确处理对象,制定了DynamicMethod标注,采用类文件的符号串格式,说明将采用动态重载机制方法的最上层类名/接口名、方法名及参数如图3所示:
图3 声明采用新机制的方法的标注及示例
它用于启动类前。处理程序可以从启动类开始,扫描项目中由编译器生成的各个类文件。记录它们的继承关系、出现的所有方法实现、这些方法的各个调用点等等。根据这些信息,处理程序要完成下列操作:
将对原方法的调用指令invokevirtual或invokeinterface改为invokedynamic,其前后的参数准备和结果返回不需修改。但invokevirtual指令和invokedynamic指令长度不一致,后续指令要作相应偏移。由此又会引起方法中局部变量、StackMap等多个属性的有效范围和转移偏移量、代码长度等数据的修改。
对每个invokedynamic指令,要指定其自举方法句柄。为此要在类文件的常量池中加入多种类型的常量,如InvokeDynamic_info、MethodHandle_info等,在类属性池中加入BootstrapMethods属性,以满足invokedynamic指令执行的需要。
还要建立一个辅助类,提供映射、自举等需要的处理代码。因为已经在编译之后,所以它要在bytecode层面直接生成。实际上各功能的具体执行过程对不同的应用是非常类似的,只是在类名、它们的继承关系、方法名、参数个数等方面有所不同,所以可以通过对一个bytecode模版进行相应的替换、重复,生成所需要的代码。在这一类中主要应包含下列方法:
根据方法签名及各个类的继承、实现信息,获得各方法句柄,并与适当的类型组合一起,建立HashMap的方法。
运行时获得实际参数类型,完成动态绑定并调用执行的方法。
指令首次调用时执行的自举方法。
对前述求直线交点的例子作如此处理,将大致生成的代码如图4所示:
图4 自动生成的辅助类
这一对类文件的修改处理步骤可以在编译后单独执行,保存获得的结果后再加以运行。也可以和程序的执行合在一起,在主程序main()启动前,代码装载时加以变换,获得的类代码直接运行。这一方案特别适宜于试验、调试阶段,虽然启动稍慢,但可重复尝试、修改。它要利用Java.lang.instrument包[4]进行,将对类代码的转换过程实现为ClassFileTransformer接口的transform方法,该方法会在类装载器装载了新类,对其进行合法性验证之前执行。在main()执行前,它可先行载入需要的类,收集信息,完成各种变换。用户还要提供一个包含premain方法的类,在其中用虚拟机提供的instrumentation对象注册该transform方法。系统启动时用适当的命令行选项,使其在应用程序的main方法之前执行。这样就可以使程序的所有类文件在完成了希望的变换后执行。
3 总结
通过使用JSR-292提供的功能,实现了在运行时才确定执行代码的方法重载机制。这对于有大量子类,而处理方式不能取决于单个对象,需要综合考虑参与处理的所有实际对象,才能最终确定的复杂系统,是很有意义的。执行代码的选择,由计算机自动执行,程序员不再需要重复编写那些繁琐的控制结构,使代码简洁清晰,易读易改,提高工作效率。这样的系统也更可重用、更易扩展,更加符合面向对象方法所追求的软件开发目标。
这种机制的使用,只需要在代码中增加一个标记。虽然实际上改变了Java的运行机制,但在语法上没有任何改变,执行结果也符合预期,不违背原有的习惯,因此不会给程序员造成任何负担。它对运行效率的影响,也只是增加一次散列表的查找,在现代运行环境下应该是可以接受的。
参考文献
[1] James Gosling, Bill Joy. The Java Language Specification Java SE 8 Edition [M]. Addison-Wesley Professional, 2015,2.
[2] John Rose. JSR-292 Supporting Dynamically Typed Languages on the Java Platform (FinalRelease) [EB/OL]. https://jcp.org/aboutJava/communityprocess/final/jsr292/.
[3] Tim Lindholm, Frank Yellin. The Java Virtual Machine Specification Java SE 8 Edition [M]. Addison-Wesley Professional ,2015,2.
[4] Oracle co. Java Platform, Standard Edition 8 API Specification [EB/OL]. http:// docs.oracle.com/javase/ 8/docs/api/.
收稿日期:(2015.04.09)
作者简介:严忠林(1964-),男,江苏镇江,上海师范大学,讲师,硕士,研究方向:Java应用、计算机体系结构,上海,200234
文章编号:1007-757X(2015)12-0069-03
中图分类号:TP311
文献标志码:A