1年前 (2023-10-23)  Java系列 |   抢沙发  218 
文章评分 0 次,平均分 0.0

在本文中,我们将探讨一个重要的API,它是在Java7中引入的,并在以后的jdk版本中得到了增强,即java.lang.invoke.MethodHandles

特别是,我们将学习什么是方法句柄(method handles),如何创建它们以及如何使用它们。

什么是方法句柄?

如API文件中所述,关于其定义:

方法句柄是对基础方法、构造函数、字段或类似低级操作的类型化、直接可执行的引用,具有参数或返回值的可选转换。

更简单地说,方法句柄是一种用于查找、调整和调用方法的低级机制

方法句柄是不可变的,并且没有可见的状态。

要创建和使用MethodHandle,需要4个步骤:

  1. 创建lookup
  2. 创建method type
  3. 查找方法句柄
  4. 调用方法句柄

方法句柄与反射

引入方法句柄是为了与现有的java.lang.reflect API一起工作,因为它们具有不同的用途和不同的特性。

从性能角度来看,MethodHandles API可能比Reflection API快得多,因为访问检查是在创建时而不是在执行时进行的。如果存在安全管理器,则这种差异会被放大,因为成员和类查找要接受额外的检查。

然而,考虑到性能并不是任务的唯一适用性度量,我们还必须考虑到,由于缺乏成员类枚举、可访问性标志检查等机制,MethodHandles API更难使用。

即便如此,MethodHandles API提供了柯里化方法、更改参数类型和更改其顺序的可能性。

有了MethodHandles API的清晰定义和目标,我们现在可以从lookup开始使用它们。

创建Lookup

当我们想要创建方法句柄时,要做的第一件事是检索查找Lookup,即负责为查找类可见的方法、构造函数和字段创建方法句柄的工厂对象。

通过MethodHandles API,可以创建具有不同访问模式的查找对象。

让我们创建一个提供对公共方法访问的查找:

MethodHandles.Lookup publicLookup = MethodHandles.publicLookup();

然而,如果我们也想访问私有和受保护的方法,我们可以使用lookup()方法:

MethodHandles.Lookup lookup = MethodHandles.lookup();

创建MethodType

为了能够创建MethodHandle,查找对象需要其类型的定义,这是通过MethodType类实现的。

特别是,MethodType表示方法句柄接受和返回的参数和返回类型,或方法句柄调用程序传递和期望的参数和返回类型。

MethodType的结构很简单,它由一个返回类型和适当数量的参数类型组成,这些参数类型必须在方法句柄及其所有调用方之间正确匹配。

与MethodHandle相同,即使是MethodType的实例也是不可变的。

让我们看看如何定义一个MethodType,该MethodType将java.util.List类指定为返回类型,将Object数组指定为输入类型:

MethodType mt = MethodType.methodType(List.class, Object[].class);

如果该方法返回基本类型或void作为其返回类型,我们将使用表示这些类型的类(void.class、int.class…)

让我们定义一个返回int值并接受Object的MethodType:

MethodType mt = MethodType.methodType(int.class, Object.class);

我们现在可以继续创建MethodHandle。

找到方法句柄

一旦我们定义了方法类型,为了创建MethodHandle,我们必须通过lookuppublicLookup对象找到它,同时提供原始类和方法名称。

特别是,查找工厂提供了一组方法,使我们能够在考虑方法范围的情况下以适当的方式找到方法句柄。从最简单的场景开始,让我们探究主要的场景。

方法的MethodHandle

使用findVirtual()方法可以为对象方法创建一个MethodHandle。让我们根据String类的concat()方法创建一个:

MethodType mt = MethodType.methodType(String.class, String.class);
MethodHandle concatMH = publicLookup.findVirtual(String.class, "concat", mt);

静态方法的方法句柄

当我们想要访问静态方法时,我们可以使用findStatic()方法:

MethodType mt = MethodType.methodType(List.class, Object[].class);

MethodHandle asListMH = publicLookup.findStatic(Arrays.class, "asList", mt);

在本例中,我们创建了一个方法句柄,用于将对象数组转换为对象列表。

构造函数的方法句柄

可以使用findConstructor()方法访问构造函数。

让我们创建一个方法句柄,它充当Integer类的构造函数,接受String属性:

MethodType mt = MethodType.methodType(void.class, String.class);

MethodHandle newIntegerMH = publicLookup.findConstructor(Integer.class, mt);

字段的方法句柄

使用方法句柄也可以访问字段。

让我们开始定义Book类:

public class Book {
    
    String id;
    String title;

    // constructor

}

先决条件是方法句柄和声明的属性之间具有直接访问可见性,我们可以创建一个充当getter的方法句柄:

MethodHandle getTitleMH = lookup.findGetter(Book.class, "title", String.class);

有关处理变量/字段的更多信息,请参阅Java 9 Variable Handles:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/invoke/VarHandle.html

私有方法的方法句柄

java.lang.reflect API的帮助下,可以为私有方法创建方法句柄。

让我们开始向Book类添加一个私有方法:

private String formatBook() {
    return id + " > " + title;
}

现在,我们可以创建一个与formatBook()方法完全相同的方法句柄:

Method formatBookMethod = Book.class.getDeclaredMethod("formatBook");
formatBookMethod.setAccessible(true);

MethodHandle formatBookMH = lookup.unreflect(formatBookMethod);

调用方法句柄

一旦我们创建了方法句柄,下一步就是使用它们。特别是,MethodHandle类提供了3种不同的方法来执行方法句柄:invoke()invokeWithAruments()invokeExact()

让我们从invoke选项开始。

当使用invoke()方法时,我们强制要固定的参数数量,但我们允许执行参数和返回类型的强制转换和装箱/取拆箱。

让我们看看如何使用带框参数的invoke()

MethodType mt = MethodType.methodType(String.class, char.class, char.class);
MethodHandle replaceMH = publicLookup.findVirtual(String.class, "replace", mt);

String output = (String) replaceMH.invoke("jovo", Character.valueOf('o'), 'a');

assertEquals("java", output);

在这种情况下,replaceMH需要char参数,但invoke()在执行之前会对Character参数执行开箱操作。

使用参数调用

使用invokeWithArguments方法调用方法句柄是三个选项中限制最小的一个。

事实上,除了参数和返回类型的强制转换和装箱/取消装箱外,它还允许变量arity调用。

在实践中,这允许我们从一个int值数组开始创建一个Integer列表:

MethodType mt = MethodType.methodType(List.class, Object[].class);
MethodHandle asList = publicLookup.findStatic(Arrays.class, "asList", mt);

List<Integer> list = (List<Integer>) asList.invokeWithArguments(1,2);

assertThat(Arrays.asList(1,2), is(list));

调用Exact

如果我们想在执行方法句柄的方式上更加严格(参数的数量及其类型),我们必须使用invokeExact()方法。

事实上,它没有为所提供的类提供任何类型转换,并且需要固定数量的参数。

让我们看看如何使用方法句柄对两个int值求和:

MethodType mt = MethodType.methodType(int.class, int.class, int.class);
MethodHandle sumMH = lookup.findStatic(Integer.class, "sum", mt);

int sum = (int) sumMH.invokeExact(1, 11);

assertEquals(12, sum);

如果在这种情况下,我们决定向invokeExact方法传递一个不是int的数字,那么调用将导致WrongMethodTypeException

使用数组

MethodHandles不仅用于字段或对象,还用于数组。事实上,使用asSpreader()API,可以生成一个数组扩展方法句柄。

在这种情况下,方法句柄接受一个数组参数,将其元素扩展为位置参数,并可以选择数组的长度。

让我们看看如何扩展方法句柄来检查数组中的元素是否相等:

MethodType mt = MethodType.methodType(boolean.class, Object.class);
MethodHandle equals = publicLookup.findVirtual(String.class, "equals", mt);

MethodHandle methodHandle = equals.asSpreader(Object[].class, 2);

assertTrue((boolean) methodHandle.invoke(new Object[] { "java", "java" }));

增强方法句柄

一旦我们定义了一个方法句柄,就可以通过将方法句柄绑定到一个参数来增强它,而无需实际调用它。

例如,在Java9中,这种行为用于优化字符串连接。

让我们看看如何执行串联,将后缀绑定到我们的concatMH

MethodType mt = MethodType.methodType(String.class, String.class);
MethodHandle concatMH = publicLookup.findVirtual(String.class, "concat", mt);

MethodHandle bindedConcatMH = concatMH.bindTo("Hello ");

assertEquals("Hello World!", bindedConcatMH.invoke("World!"));

Java 9增强功能

在Java9中,对MethodHandles API进行了一些增强,目的是使其更易于使用。

这些增强影响了3个主要主题:

  • 查找函数–允许从不同上下文中查找类,并支持接口中的非抽象方法
  • 参数处理——改进参数折叠、参数收集和参数传播功能
  • 附加组合–添加循环(loopwhileLoopdoWhileLoop…),并通过tryFinally提供更好的异常处理支持

这些变化带来了一些额外的好处:

  • 增加JVM编译器优化
  • 实例化减少
  • 在使用MethodHandles API时启用了精度

有关所做增强的详细信息,请参阅MethodHandles API Javadoc:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/invoke/MethodHandles.html

结论

在本文中,我们介绍了MethodHandles API、它们是什么以及如何使用它们。

我们还讨论了它与反射API的关系,由于方法句柄允许低级别操作,因此最好避免使用它们,除非它们完全适合工作范围。

源码地址:https://github.com/eugenp/tutorials/tree/master/core-java-modules/core-java-9

原文链接:https://www.baeldung.com/java-method-handles

 

除特别注明外,本站所有文章均为老K的Java博客原创,转载请注明出处来自https://javakk.com/2914.html

关于

发表评论

表情 格式

暂无评论

登录

忘记密码 ?

切换登录

注册