【Java 技术专题】「攻破技术盲区」带你攻破你很可能存在的 Java 技术盲点之动态性技术原理指南(方法句柄—基础篇)
前提介绍
本节内容介绍 Java 的一个重要新特性,它对 Java 虚拟机规范进行了修改,而非 Java 语言规范。相比之前提到的 Java 7 的新特性,这个修改更为复杂,对 Java 平台的影响也更深远。
反射能力的增强
Java 虚拟机中方法调用的支持得到了增强,这一改动虽然最初是为了更好地支持动态语言编译器,但它对普通应用程序的影响也是极为重要的。这一改动为我们提供了比反射 API 更为强大的动态方法调用能力。本节将详细介绍 JSR 292 (Supporting Dynamically Typed Language) 中的 Java 7 重要特性,包括 Java 虚拟机中新的调用指令 invoke dynamic,以及 Java SE 7 核心库中的 java.lang.invoke 包。
Java 虚拟机与 Java 源码
在介绍 Java 虚拟机的新特性之前,需要先简单介绍一下它的工作原理。Java 虚拟机本身并不知道 Java 语言的存在,它只能理解 Java 字节代码格式,即 class 文件中包含的指令和符号表。Java 虚拟机的主要职责是执行 class 文件中的指令,这些文件可以由 Java 语言的编译器生成,也可以由其他编程语言的编译器生成,甚至可以通过手动工具生成。只要 class 文件格式符合规范,虚拟机就能正确地执行它们。
Java 虚拟机能力支持
Java 虚拟机实际上将操作系统和应用程序之间添加了一个新的抽象层次。传统上,一种编程语言需要将源代码直接编译成目标平台上的机器代码以获得最高的效率。然而,这种方法存在一些问题,例如生成的二进制代码无法兼容不同平台,实现的复杂度较高等。使用虚拟机来解决这些问题会更加简单和高效。
虚拟机提供一个抽象层次,屏蔽了底层系统的差异,其所暴露的接口是规范和统一的,可以实现“编写一次,到处运行”的目标。
虚拟机提供了需要的运行时支持能力,包括内存管理、安全机制、并发控制、标准库和工具等。
使用现有的虚拟机作为运行平台,编程语言使用者可以复用已有的相关资产,包括相关工具、集成开发环境和开发经验。这有利于编程语言本身的推广和普及。
Java 的多语言支持
许多编程语言都支持 Java 虚拟机作为目标运行平台。这些语言的编译器可以将源代码编译成 Java 字节码。流行的语言包括 Java、Scala、JRuby、Groovy、Jython、PHP、C#、JavaScript、Tcl 和 Lisp 等。其中,Java 语言本身是最流行的。
Java 动态性的局限性
尽管 Java 虚拟机不关心字节代码的编写语言,但 Java 语言作为虚拟机上最重要的语言,对 Java 虚拟机规范产生了最大的影响,许多特性都是为了配合 Java 语言而产生的。因为 Java 语言是一门静态类型的编程语言,所以对 Java 虚拟机的动态性也造成了影响。虽然越来越多的动态类型编程语言采用 Java 虚拟机作为运行平台,但 Java 虚拟机本身对动态性的支持不足,导致这些动态类型语言在实现时会遇到一些阻碍。然而,动态类型语言的实现者总是可以找到方法规避 Java 虚拟机的限制。
方法句柄处理操作
Java7 引入了动态语言支持,对 Java 虚拟机规范进行了修改,使得 Java 虚拟机更加友好并且性能更好。动态语言支持涉及到应用程序中最常见的方法调用,主要包括 Java 标准库中新的方法调用 API 和 Java 虚拟机规范中新的 invokedynamic 指令。方法句柄是这一部分的起点,而 Java API 则是开发者最常用的部分。接下来,将介绍方法句柄以及 invokedynamic 指令。
方法句柄
方法句柄是 JSR292 中引入的概念,它是 Java 中方法、构造方法和域的一个强类型可执行引用,句柄即为其含义。使用方法句柄可以直接调用底层方法。方法句柄相当于反射 API 中的 Method 类,但更加强大、灵活、高效。在 Java 标准库中,方法句柄使用 java.lang.invoke.MethodHandle 类表示。方法句柄和反射 API 也可以协同使用。
方法句柄类型
方法句柄的类型选择
一个方法句柄的类型只与其参数类型和返回值类型相关,与其所引用的底层方法名称和所在的类无关。
例如,引用 String 类的 length 方法和 Integer 类的 intValue 方法的方法句柄类型相同,因为两者都没有参数且返回类型都是 int。
MethodType 方法类型
在获取方法句柄(即 MethodHandle 类的对象)后,可以通过其 type 方法查看其类型。该方法返回一个 java.lang.invoke.MethodType 类的对象。
MethodType 类的所有实例都是不可变的,类似于 String 类。对 MethodType 类对象的任何修改都会生成一个新的 MethodType 类对象。MethodType 类对象是否相等取决于它们所包含的参数类型和返回值类型是否完全一致。
MethodType 的创建方法
MethodType 类的实例只能通过 MethodType 类中的静态工厂方法来创建。这些工厂方法分为三类。
第一类工厂方法是通过指定参数和返回值类型来创建 MethodType,主要是使用 methodType 方法的多个重载形式。在使用这些方法时,必须至少指定返回值类型,而参数类型可以是 0 个至多个。
返回值类型总是出现在 methodType 方法参数列表的第一个位置,后面是 0 个至多个参数类型。类型由 Class 类的对象指定。如果返回值类型是 void,可以使用 void.class 或 java.lang.Void.class 进行声明。
代码示例如下
直接方式进行获取方法句柄
值得注意的是,在最后一个 methodType 方法调用中,使用另一个 MethodType 的参数类型作为当前 MethodType 对象的参数类型。
引用方式进行获取方法句柄(genericMethodType)
除了显式地指定返回值和参数类型之外,还可以创建通用的 MethodType 类型,其中返回值和所有参数的类型都是 Object 类。
可以使用静态工厂方法 genericMethodType 来创建。方法 genericMethodType 有两种重载形式:
第一种形式只需要指明方法类型中包含的 Object 类型的参数个数即可。
第二种形式可以提供一个额外的参数来说明是否在参数列表的最后添加一个 Object 类型的参数。
生成通用 MethodType 类型的示例
例如,mt1 有 3 个类型为 Object 的参数,而 mt2 有 2 个类型为 Object 的参数和后面的 Object 类型参数。
fromMethodDescriptorString
介绍的另一个工厂方法是 fromMethodDescriptorString,这个方法允许开发人员指定方法类型在字节码中的表示形式。方法的参数是一个描述符字符串,它描述了返回值和参数类型。描述符字符串的格式如下:
其中,参数类型可以是任意的基本类型(例如 I 表示整型,D 表示双精度浮点类型等等),也可以是引用类型的全限定名(例如 Ljava/lang/String;表示 String 类型)。返回值类型也可以是任意的基本类型和引用类型的全限定名。
使用方法类型在字节代码中的表示形式来创建 Method Type
例如,String.getChars 方法的类型在字节码中的表示形式为“(II[CI)V”,其中“(II[CI)”表示三个参数的类型,分别是 int、int、char[]和 int,而“V”表示返回值类型为 void。这种格式比逐个声明返回值和参数类型要更简洁,适合于对 Java 字节码格式比较熟悉的开发人员。
“(Ljava/lang/String;)Ljava/lang/String;” 所表示的方法类型是返回值和参数类型都是 java.lang.String,相当于使用 MethodType.methodType(String.class, String.class)。
fromMethodDescriptorString 的类加载器
在使用 fromMethodDescriptorString 方法的时候,需要指定一个类加载器来加载方法类型表达式中出现的 Java 类,如果不指定,默认使用系统类加载器。
对 MethodType 中的返回值和参数类型进行修改的示例
创建出 MethodType 对象实例之后,可以对其进行进一步的修改,包括改变返回值类型、添加和插入新参数、删除已有参数和修改已有参数的类型等。这些修改操作对应的方法会返回一个新的 MethodType 对象。
修改返回值和参数类型的示例代码。在每个修改方法的注释中,都给出了修改之后的类型,其中括号内是参数类型列表,而括号外是返回值类型。
一次性修改 MethodType 中的返回值和所有参数的类型的示例
除了上面提到的精确修改返回值和参数类型的方法,MethodType 还有一些方法可以一次性处理返回值和所有参数的类型。
这几个方法的示例:wrap 和 unwrap 用于基本类型与包装类型之间的转换;generic 方法会将返回值和参数类型都转换为 Object 类型;erase 方法只会将引用类型转换为 Object 类型,而不作处理基本类型。以下是修改之后的方法类型:
因为每个对 MethodType 对象进行修改的方法都会返回一个新的 MethodType 对象,所以可以使用方法级联来简化代码。
方法句柄的调用
方法句柄提供了一种灵活的调用方法,类似于反射 API 中的 Method 类。可以通过获取方法句柄来直接调用底层方法,最直接的方式就是使用 invokeExact 方法。
invokeExact 方法接收两个参数,第一个是作为方法接收者的对象,第二个是调用时的实际参数列表。
使用开发案例
举个例子,假设我们获取了 String 类中 substring 方法的方法句柄,在代码中可以通过 invokeExact 来直接调用该方法,就相当于直接调用"Hello World".substring(1,3)。
使用 invokeExact 方法调用方法句柄
强调一下静态方法和一般方法之间的区别,静态方法在调用时不需要指定方法的接收对象,而一般的方法则需要指定接收对象。如果方法句柄引用的是 java.lang.Math 类中的静态方法 min,那么可以直接通过 mh.invokeExact(3, 4)来调用该方法。
注意,使用 invokeExact 方法调用方法时,要求严格匹配方法的参数类型和返回值类型。上面代码中方法句柄引用的 substring 方法的返回类型是 String。因此,在使用 invokeExact 方法进行调用时,需要在调用表达式前面加上强制类型转换,以声明返回值的类型。如果省略了类型转换并直接将返回值赋值给 Object 类型的变量,在调用时会抛出异常,因为 invokeExact 会默认方法返回值类型为 Object 类型。同样,省略类型转换而不进行赋值操作也是错误的,因为 invokeExact 会将方法返回值类型视为 void 类型,而不是方法句柄所要求的 String 类型。
使用 invoke 方法调用方法句柄
与 invokeExact 方法要求严格匹配的类型不同,invoke 方法允许使用更加宽松的类型。
invoke 方法的实现原理
在调用时,它会尝试转换返回值和参数的类型。这是通过 MethodHandle 类的 asType 方法来实现的。asType 方法将当前的方法句柄适配到新的 MethodType 上,并生成一个新的方法句柄。
如果方法句柄在调用时的类型与其声明的类型完全一致,调用 invoke 就等同于调用 invokeExact;否则,invoke 会先调用 asType 方法来尝试适配到调用时的类型。
如果适配成功,调用将继续执行;否则会抛出相关的异常。这种灵活的适配机制使得 invoke 方法成为在绝大多数情况下都应该使用的方法句柄调用方式。
进行类型适配时,基本的规则是比较返回值类型和每个参数的类型是否都可以相互匹配。只要返回值类型或某个参数的类型无法完成匹配,整个适配过程就会失败。
待转换的源类型 S 到目标类型 T 匹配成功的基本原则
如果源类型 S 和目标类型 T 相同,则匹配成功;
如果源类型 S 是目标类型 T 的子类型,则匹配成功;
如果源类型 S 和目标类型 T 都是原始类型,则根据 Java 的原始类型转换规则来匹配;
如果源类型 S 和目标类型 T 都是引用类型,则根据 Java 的引用类型转换规则来匹配;
如果源类型 S 是一个原始类型,且目标类型 T 是一个对应的包装类型,或反之亦然,则匹配成功;
如果源类型 S 可以通过拆箱操作转换为一个基本类型,且该基本类型可以通过装箱操作转换为目标类型 T,则匹配成功;
在上述情况下都无法匹配成功时,就会抛出 NoSuchMethodError 或 IllegalArgumentException 异常。
转换两个方法类型的规则可以简述为:只要源类型中的返回值类型和参数类型都可以分别对应到目标类型中的返回值类型和参数类型,那么就可以进行类型转换。使用 invoke 方法时,只需要将上面的代码中的 invokeExact 方法替换成 invoke 方法即可,不需要做太多的介绍。
invokeWithArguments
使用 invokeWithArguments 方法。该方法在调用时可以指定任意多个 Object 类型的参数。
具体方式是先根据传入的实际参数个数,使用 MethodType 的 genericMethodType 方法得到一个返回值和参数类型都是 Object 的新方法类型。然后将原始的方法句柄通过 asType 方法转换成新的方法句柄。
最后通过新方法句柄的 invokeExact 方法来完成调用。相对于 invokeExact 和 invoke 方法,invokeWithArguments 方法的优势在于,它可以通过 Java 反射 API 被正常获取和调用,而 invokeExact 和 invoke 方法则不能这样使用。因此,invokeWithArguments 方法可以作为反射 API 和方法句柄之间的桥梁。
评论