【Java技术专题】「攻破技术盲区」带你攻破你很可能存在的Java技术盲点之动态性技术原理指南(方法句柄—基础篇)

前提介绍

本节内容介绍Java 7的一个重要新特性,它对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虚拟机实际上将操作系统和应用程序之间添加了一个新的抽象层次。传统上,一种编程语言需要将源代码直接编译成目标平台上的机器代码以获得最高的效率。然而,这种方法存在一些问题,例如生成的二进制代码无法兼容不同平台,实现的复杂度较高等。使用虚拟机来解决这些问题会更加简单和高效。

  1. 虚拟机提供一个抽象层次,屏蔽了底层系统的差异,其所暴露的接口是规范和统一的,可以实现"编写一次,到处运行"的目标。

  2. 虚拟机提供了需要的运行时支持能力,包括内存管理、安全机制、并发控制、标准库和工具等。

  3. 使用现有的虚拟机作为运行平台,编程语言使用者可以复用已有的相关资产,包括相关工具、集成开发环境和开发经验。这有利于编程语言本身的推广和普及。

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进行声明。

代码示例如下
java 复制代码
   MethodType type1 = MethodType.methodType(void.class);  // 对应 String voidMethod()
   MethodType type2 = MethodType.methodType(int.class);   // 对应 String length()
   MethodType type3 = MethodType.methodType(String.class, int.class); // 对应 String substring(int)
   MethodType type4 = MethodType.methodType(String.class, CharSequence.class); // 对应 String concat(CharSequence)
   MethodType type5 = MethodType.methodType(type1, String.class); // 在 type5 中使用另一个 MethodType 的参数类型作为当前类型的参数类型
直接方式进行获取方法句柄

值得注意的是,在最后一个methodType方法调用中,使用另一个MethodType的参数类型作为当前MethodType对象的参数类型。

java 复制代码
public void generateMethodTypes(){
	//String.length (
	MethodType mt1 = MethodType.methodType (int.class);
	//String.concat(String str)
	MethodType mt2 = MethodType.methodType(String.class,String.class);
	//String.getChars (int srcBegin,int srcEnd,char[]dst,int dstBegin)
	MethodType mt3 = MethodType.methodType(void.class,int.class,int.class,
	char[].class,int.class);
	//String.startswith (String prefix)
	MethodType mt4 = MethodType.methodType (boolean.class,mt2);
}
引用方式进行获取方法句柄(genericMethodType)

除了显式地指定返回值和参数类型之外,还可以创建通用的MethodType类型,其中返回值和所有参数的类型都是Object类。

可以使用静态工厂方法genericMethodType来创建。方法genericMethodType有两种重载形式:

  • 第一种形式只需要指明方法类型中包含的Object类型的参数个数即可。
  • 第二种形式可以提供一个额外的参数来说明是否在参数列表的最后添加一个Object类型的参数。
生成通用MethodType类型的示例

例如,mt1有3个类型为Object的参数,而mt2有2个类型为Object的参数和后面的Object类型参数。

java 复制代码
	// 返回值和参数都是Object类型,其中有3个Object类型的参数
   MethodType mt1 = MethodType.genericMethodType(3);  
	// 返回值和参数都是Object类型,其中有2个Object类型的参数和一个后面的Object类型的参数(即参数列表的最后一个参数为Object类型)   
   MethodType mt2 = MethodType.genericMethodType(2, true); 

fromMethodDescriptorString

介绍的另一个工厂方法是fromMethodDescriptorString,这个方法允许开发人员指定方法类型在字节码中的表示形式。方法的参数是一个描述符字符串,它描述了返回值和参数类型。描述符字符串的格式如下:

java 复制代码
  (<参数类型1><参数类型2>...)<返回值类型>

其中,参数类型可以是任意的基本类型(例如I表示整型,D表示双精度浮点类型等等),也可以是引用类型的全限定名(例如Ljava/lang/String;表示String类型)。返回值类型也可以是任意的基本类型和引用类型的全限定名。

使用方法类型在字节代码中的表示形式来创建Method Type

例如,String.getChars方法的类型在字节码中的表示形式为"(II[CI)V",其中"(II[CI)"表示三个参数的类型,分别是int、int、char[]和int,而"V"表示返回值类型为void。这种格式比逐个声明返回值和参数类型要更简洁,适合于对Java字节码格式比较熟悉的开发人员。

java 复制代码
public void generateMethodTypesFromDescriptor(){
	classLoader cl = this.getclass()getclassLoader();
	String descriptor = "(Ljava/lang/String;)Ljava/lang/String;";
	MethodType mt1 = MethodType.fromMethodDescriptorstring(descriptor,cl);
}

"(Ljava/lang/String;)Ljava/lang/String;" 所表示的方法类型是返回值和参数类型都是java.lang.String,相当于使用MethodType.methodType(String.class, String.class)

fromMethodDescriptorString的类加载器

在使用fromMethodDescriptorString方法的时候,需要指定一个类加载器来加载方法类型表达式中出现的Java类,如果不指定,默认使用系统类加载器。

对MethodType中的返回值和参数类型进行修改的示例

创建出MethodType对象实例之后,可以对其进行进一步的修改,包括改变返回值类型、添加和插入新参数、删除已有参数和修改已有参数的类型等。这些修改操作对应的方法会返回一个新的MethodType对象。

java 复制代码
public void changeMethodType(){
	//(int,int)string
	MethodType mt MethodType.methodType(String.class,int.class,int.class);
	//(int,int,float)string
	mtmt.appendParameterTypes (float.class);
	//(int,double,long,int,float)string
	mtmt.insertParameterTypes (1,double.class,long.class);
	//(int,double,int,float)string
	mt mt.dropParameterTypes(2,3);
	//(int,double,String,float)string
	mt =mt.changeParameterType(2,String.class);
	//(int,double,String,float)void
	mt mt.changeReturnType (void.class);
}

修改返回值和参数类型的示例代码。在每个修改方法的注释中,都给出了修改之后的类型,其中括号内是参数类型列表,而括号外是返回值类型。

一次性修改MethodType中的返回值和所有参数的类型的示例

除了上面提到的精确修改返回值和参数类型的方法,MethodType还有一些方法可以一次性处理返回值和所有参数的类型。

这几个方法的示例:wrap和unwrap用于基本类型与包装类型之间的转换;generic方法会将返回值和参数类型都转换为Object类型;erase方法只会将引用类型转换为Object类型,而不作处理基本类型。以下是修改之后的方法类型:

java 复制代码
public void wrapAndGeneric(){
	//(int,double)Integer
	MethodType mt = MethodType.methodType(Integer.class,int.class,double.class);
	//(Integer,Double)Integer
	MethodType wrapped = mt.wrap();
	//(int,double)int
	MethodType unwrapped = mt.unwrap();
	//(object,Object)object
	MethodType generic = mt.generic();
	//(int,double)object
	MethodType erased = mt.erase ()
}

因为每个对MethodType对象进行修改的方法都会返回一个新的MethodType对象,所以可以使用方法级联来简化代码。

方法句柄的调用

方法句柄提供了一种灵活的调用方法,类似于反射API中的Method类。可以通过获取方法句柄来直接调用底层方法,最直接的方式就是使用invokeExact方法

invokeExact方法接收两个参数,第一个是作为方法接收者的对象,第二个是调用时的实际参数列表。

使用开发案例

举个例子,假设我们获取了String类中substring方法的方法句柄,在代码中可以通过invokeExact来直接调用该方法,就相当于直接调用"Hello World".substring(1,3)。

使用invokeExact方法调用方法句柄

java 复制代码
public void invokeExact ()throws Throwable{
	MethodHandles.Lookuplookup = MethodHandles.lookup ();
	MethodType type = MethodType.methodType(String.class,int.class,int.class);
	MethodHandle mh = lookup.findvirtual(String.class,"substring",type);
	String str = (String)mh.invokeExact ("Hello World",1,3);
	System.out.printin(str);
}

强调一下静态方法和一般方法之间的区别,静态方法在调用时不需要指定方法的接收对象,而一般的方法则需要指定接收对象。如果方法句柄引用的是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和方法句柄之间的桥梁。

相关推荐
武大打工仔3 分钟前
从零开始手搓一个MVC框架
后端
开心猴爷8 分钟前
移动端网页调试实战 Cookie 丢失问题的排查与优化
后端
kaika18 分钟前
告别复杂配置!使用 1Panel 运行环境功能轻松搭建 Java 应用
java·1panel·建站·halo
用户5724056148 分钟前
解析Json
后端
舒一笑9 分钟前
Mac 上安装并使用 frpc(FRP 内网穿透客户端)指南
后端·网络协议·程序员
每天学习一丢丢15 分钟前
Spring Boot + Vue 项目用宝塔面板部署指南
vue.js·spring boot·后端
邹小邹15 分钟前
Go 1.25 强势来袭:GC 速度飙升、并发测试神器上线,内存检测更精准!
后端·go
有梦想的攻城狮16 分钟前
Java 11中的Collections类详解
java·windows·python·java11·collections
lichenyang45319 分钟前
管理项目服务器连接数据库
数据库·后端
生无谓21 分钟前
在Windows系统上安装多个JDK版本并切换
后端