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

本文涉及的产品
文本翻译,文本翻译 100万字符
图片翻译,图片翻译 100张
文档翻译,文档翻译 1千页
简介: 【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和方法句柄之间的桥梁。

相关文章
|
4天前
|
存储 监控 安全
单位网络监控软件:Java 技术驱动的高效网络监管体系构建
在数字化办公时代,构建基于Java技术的单位网络监控软件至关重要。该软件能精准监管单位网络活动,保障信息安全,提升工作效率。通过网络流量监测、访问控制及连接状态监控等模块,实现高效网络监管,确保网络稳定、安全、高效运行。
30 11
|
14天前
|
XML Java 编译器
Java注解的底层源码剖析与技术认识
Java注解(Annotation)是Java 5引入的一种新特性,它提供了一种在代码中添加元数据(Metadata)的方式。注解本身并不是代码的一部分,它们不会直接影响代码的执行,但可以在编译、类加载和运行时被读取和处理。注解为开发者提供了一种以非侵入性的方式为代码提供额外信息的手段,这些信息可以用于生成文档、编译时检查、运行时处理等。
50 7
|
24天前
|
消息中间件 Java Kafka
在Java中实现分布式事务的常用框架和方法
总之,选择合适的分布式事务框架和方法需要综合考虑业务需求、性能、复杂度等因素。不同的框架和方法都有其特点和适用场景,需要根据具体情况进行评估和选择。同时,随着技术的不断发展,分布式事务的解决方案也在不断更新和完善,以更好地满足业务的需求。你还可以进一步深入研究和了解这些框架和方法,以便在实际应用中更好地实现分布式事务管理。
|
14天前
|
JavaScript 安全 Java
java版药品不良反应智能监测系统源码,采用SpringBoot、Vue、MySQL技术开发
基于B/S架构,采用Java、SpringBoot、Vue、MySQL等技术自主研发的ADR智能监测系统,适用于三甲医院,支持二次开发。该系统能自动监测全院患者药物不良反应,通过移动端和PC端实时反馈,提升用药安全。系统涵盖规则管理、监测报告、系统管理三大模块,确保精准、高效地处理ADR事件。
|
23天前
|
安全 Java 开发者
Java中WAIT和NOTIFY方法必须在同步块中调用的原因
在Java多线程编程中,`wait()`和`notify()`方法是实现线程间协作的关键。这两个方法必须在同步块或同步方法中调用,这一要求背后有着深刻的原因。本文将深入探讨为什么`wait()`和`notify()`方法必须在同步块中调用,以及这一机制如何确保线程安全和避免死锁。
37 4
|
23天前
|
Java
深入探讨Java中的中断机制:INTERRUPTED和ISINTERRUPTED方法详解
在Java多线程编程中,中断机制是协调线程行为的重要手段。了解和正确使用中断机制对于编写高效、可靠的并发程序至关重要。本文将深入探讨Java中的`Thread.interrupted()`和`Thread.isInterrupted()`方法的区别及其应用场景。
24 4
|
21天前
|
Java 数据处理 数据安全/隐私保护
Java处理数据接口方法
Java处理数据接口方法
24 1
|
5天前
|
安全 Java API
java如何请求接口然后终止某个线程
通过本文的介绍,您应该能够理解如何在Java中请求接口并根据返回结果终止某个线程。合理使用标志位或 `interrupt`方法可以确保线程的安全终止,而处理好网络请求中的各种异常情况,可以提高程序的稳定性和可靠性。
35 6
|
20天前
|
设计模式 Java 开发者
Java多线程编程的陷阱与解决方案####
本文深入探讨了Java多线程编程中常见的问题及其解决策略。通过分析竞态条件、死锁、活锁等典型场景,并结合代码示例和实用技巧,帮助开发者有效避免这些陷阱,提升并发程序的稳定性和性能。 ####
|
18天前
|
存储 监控 小程序
Java中的线程池优化实践####
本文深入探讨了Java中线程池的工作原理,分析了常见的线程池类型及其适用场景,并通过实际案例展示了如何根据应用需求进行线程池的优化配置。文章首先介绍了线程池的基本概念和核心参数,随后详细阐述了几种常见的线程池实现(如FixedThreadPool、CachedThreadPool、ScheduledThreadPool等)的特点及使用场景。接着,通过一个电商系统订单处理的实际案例,分析了线程池参数设置不当导致的性能问题,并提出了相应的优化策略。最终,总结了线程池优化的最佳实践,旨在帮助开发者更好地利用Java线程池提升应用性能和稳定性。 ####