方法句柄简介

此页面由 Nataliia DziubenkoUPL 下贡献

 

什么是方法句柄

方法句柄是一种用于方法查找和调用的低级机制。它们通常与反射进行比较,因为反射 API 和方法句柄都提供了一种调用方法、构造函数和访问字段的方法。

方法句柄到底是什么?它是一个可以直接调用的对底层方法、构造函数或字段的引用。方法句柄 API 允许在指向方法的简单指针之上进行操作,使我们能够插入或重新排序参数、转换返回值等。

让我们仔细看看方法句柄可以提供什么以及如何有效地使用它们。

 

访问检查

与反射 API 相比,方法句柄调用的访问检查方式不同。使用反射,每次调用都会对调用者进行访问检查。对于方法句柄,只有在创建方法句柄时才会进行访问检查。

重要的是要记住,如果方法句柄是在可以访问非公共成员的上下文中创建的,那么在传递到外部时,它仍然可以访问这些非公共成员。因此,非公共成员可能从不应该访问它们的代码中被访问。开发人员有责任将这些方法句柄保留在其上下文中。或者,可以使用适当的查找对象立即创建具有访问限制的方法句柄。

 

方法句柄查找

要创建方法句柄,我们首先需要创建一个 Lookup 对象,它充当创建方法句柄的工厂。根据查找对象本身或方法句柄的使用方式,我们可以决定是否应该限制其访问级别。

例如,如果我们创建了一个指向私有方法的方法句柄,并且该方法句柄可以从外部访问,那么私有方法也可以访问。通常,我们希望避免这种情况。一种方法是将查找对象和方法句柄也设为private。另一种选择是使用 MethodHandles.publicLookup 方法创建查找对象,这样它就只能搜索无条件导出的包中公共类中的公共成员。

MethodHandles.Lookup publicLookup = MethodHandles.publicLookup();

如果我们要将查找对象和方法句柄保留为私有,那么让它们访问任何成员(包括私有和受保护的成员)是安全的。

MethodHandles.Lookup lookup = MethodHandles.lookup();

 

方法类型

要查找方法句柄,我们还需要提供方法或字段的类型信息。方法类型信息表示为 MethodType 对象。要实例化 MethodType,我们必须提供返回类型作为第一个参数,然后是所有参数类型。

MethodType methodType = MethodType.methodType(int.class /* the method returns integer */,
        String.class /* and accepts a single String argument*/);

有了 LookupMethodType 实例,我们就可以查找方法句柄。对于实例方法,我们应该使用 Lookup.findVirtual,对于静态方法,我们应该使用 Lookup.findStatic。这两种方法都接受以下参数:方法所在的 Class、表示为 String 的方法名称和 MethodType 实例。

在下面的示例中,我们使用 Lookup.findVirtual 方法查找实例方法 String.replace,它接受两个 char 参数并返回一个 String

MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodType replaceMethodType = MethodType.methodType(String.class, char.class, char.class);
MethodHandle replaceMethodHandle = lookup.findVirtual(String.class, "replace", replaceMethodType);

在下一个示例中,我们使用 Lookup.findStatic 查找静态方法 String.valueOf,它接受一个 Object 并返回一个 String

MethodType valueOfMethodType = MethodType.methodType(String.class, Object.class);
MethodHandle valueOfMethodHandle = lookup.findStatic(String.class, "valueOf", valueOfMethodType);

类似地,我们可以使用 Lookup.findConstructor 方法查找指向任何构造函数的方法句柄。

最后,当我们获得方法句柄后,就可以调用底层方法。

 

方法句柄调用

调用也可以通过多种方式完成。

所有用于调用方法最终都会归结为一个在最后调用的方法:MethodHandle.invokeExact。顾名思义,提供给 invokeExact 方法的参数必须严格匹配方法句柄的类型。

例如,如果我们调用 String.replace 方法,则参数必须严格对应于 String 返回类型和两个 char 参数。

MethodType replaceMethodType = MethodType.methodType(String.class, char.class, char.class);
MethodHandle replaceMethodHandle = lookup.findVirtual(String.class, "replace", replaceMethodType);
String result = (String) replaceMethodHandle.invokeExact("dummy", 'd', 'm');

MethodHandle.invoke 更加宽松。它尝试获取一个具有调整类型的新方法句柄,该类型将严格匹配提供的参数的类型。之后,它将能够使用 invokeExact 调用调整后的方法句柄。

String result = (String) replaceMethodHandle.invoke((Object)"dummy", (Object)'d', (Object)'m'); // would fail with `invokeExact`

调用方法句柄的另一种方法是使用 MethodHandle.invokeWithArguments。此方法调用的结果等效于 invoke,唯一的区别是所有参数都可以作为对象数组或列表提供。

此方法的一个有趣特性是,如果提供的参数数量超过预期数量,则所有剩余参数将被压缩到最后一个参数中,该参数将被视为数组。

 

访问字段

可以创建对字段具有读或写访问权限的方法句柄。对于实例字段,这是通过 findGetterfindSetter 方法实现的,对于静态字段,则是通过 findStaticGetterfindStaticSetter 方法实现的。我们不需要提供 MethodType 实例;相反,我们应该提供一个单一类型,即字段的类型。

例如,如果我们的 Example 类中有一个静态字段 magic

private static String magic = "initial value static field";

假设我们已经创建了一个 Lookup 对象

MethodHandles.Lookup lookup = MethodHandles.lookup();

我们可以简单地创建 setter 和 getter 方法句柄,并分别调用它们。

MethodHandle setterStaticMethodHandle = lookup.findStaticSetter(Example.class, "magic", String.class);
MethodHandle getterStaticMethodHandle = lookup.findStaticGetter(Example.class, "magic", String.class);

setterStaticMethodHandle.invoke("new value static field");
String staticFieldResult = (String) getterStaticMethodHandle.invoke(); // staticFieldResult == `new value static field`

这是一个 Example 类的实例字段 abc

private String abc = "initial value";

我们可以类似地为实例字段的读写创建方法句柄。

MethodHandle setterMethodHandle = lookup.findSetter(Example.class, "abc", String.class);
MethodHandle getterMethodHandle = lookup.findGetter(Example.class, "abc", String.class);

要将 setter 和 getter 方法句柄与实例字段一起使用,我们必须首先获取字段所属类的实例。

Example example = new Example();

之后,我们必须为 setter 和 getter 的调用提供 Example 的实例。

setterMethodHandle.invoke(example, "new value");
String result = (String) getterMethodHandle.invoke(example); // result == `new value`

虽然可以使用方法句柄读取和写入字段值,但这并不常见。对于字段,更适合使用 VarHandle,可以使用 findVarHandlefindStaticVarHandle 方法创建。

 

使用数组

MethodHandles 类包含提供许多预设方法句柄的方法。这些包括允许数组操作的方法句柄。创建这些方法句柄不需要访问检查,因此不需要查找对象。

让我们使用 arrayConstructor 创建一个包含 5 个元素的字符串数组。

MethodHandle arrayConstructor = MethodHandles.arrayConstructor(String[].class);
String[] arr = (String[]) arrayConstructor.invoke(5);

要修改单个元素,我们可以使用 arrayElementSetter,我们向它提供目标数组的引用、元素的索引和新值。

MethodHandle elementSetter = MethodHandles.arrayElementSetter(String[].class);
elementSetter.invoke(arr, 4, "test");

要读取单个元素的值,我们应该使用 arrayElementGetter 方法句柄,我们向它提供数组的引用和元素索引。

MethodHandle elementGetter = MethodHandles.arrayElementGetter(String[].class);
String element = (String) elementGetter.invoke(arr, 4); // element == "test"

我们还可以使用 arrayLength 提供的方法句柄以整数形式获取数组长度。

MethodHandle arrayLength = MethodHandles.arrayLength(String[].class);
int length = (int) arrayLength.invoke(arr); // length == 5

 

异常处理

invokeExactinvoke 都抛出 Throwable,因此对底层方法可以抛出的内容没有限制。调用方法句柄的方法必须显式抛出 Throwable 或捕获它。

MethodHandles API 中的某些方法可以使异常处理更容易。让我们看几个例子。

catch 包装器

MethodHandles.catchException 方法可以在提供的异常处理程序方法句柄中包装给定的方法句柄。

假设我们有一个执行某些业务逻辑的方法 problematicMethod,以及一个处理特定 IllegalArgumentException 的方法 exceptionHandler。异常处理程序方法必须返回与原始方法相同的类型。它接受的第一个参数是我们要处理的 Throwable,之后是最初接受的其余参数。

public static int problematicMethod(String argument) throws IllegalArgumentException {
    if ("invalid".equals(argument)) {
        throw new IllegalArgumentException();
    }
    return 1;
}

public static int exceptionHandler(IllegalArgumentException e, String argument) {
    // log exception
    return 0;
}

我们可以查找这两个方法的方法句柄,并将 problematicMethod 包装在 exceptionHandler 中。生成的 MethodHandle 将在调用时正确处理 IllegalArgumentException,如果出现其他异常,则继续抛出这些异常。

MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle methodHandle = lookup.findStatic(Example.class, "problematicMethod", MethodType.methodType(int.class, String.class));
MethodHandle handler = lookup.findStatic(Example.class, "exceptionHandler",
        MethodType.methodType(int.class, IllegalArgumentException.class, String.class));
MethodHandle wrapped = MethodHandles.catchException(methodHandle, IllegalArgumentException.class, handler);

System.out.println(wrapped.invoke("valid")); // outputs "1"
System.out.println(wrapped.invoke("invalid")); // outputs "0"

finally 包装器

MethodHandles.tryFinally 方法的工作原理类似,但它不是异常处理程序,而是包装目标方法,添加一个 try-finally 块。

假设我们有一个单独的方法 cleanupMethod 包含清理逻辑。此方法的返回类型必须与目标方法的返回类型相同。它必须接受一个 Throwable,然后是来自目标方法的结果值,最后是所有参数。

public static int cleanupMethod(Throwable e, int result, String argument) {
    System.out.println("inside finally block");
    return result;
}

我们可以将前面示例中的方法句柄包装在 try-finally 块中,如下所示

MethodHandle cleanupMethod = lookup.findStatic(Example.class, "cleanupMethod",
        MethodType.methodType(int.class, Throwable.class, int.class, String.class));

MethodHandle wrappedWithFinally = MethodHandles.tryFinally(methodHandle, cleanupMethod);

System.out.println(wrappedWithFinally.invoke("valid")); // outputs "inside finally block" and "1"
System.out.println(wrappedWithFinally.invoke("invalid")); // outputs "inside finally block" and throws java.lang.IllegalArgumentException 

 

方法句柄转换

从前面的示例可以看出,方法句柄可以封装比简单地指向底层方法更多的行为。我们可以获得 **适配器** 方法句柄,它包装目标方法句柄以添加某些行为,例如参数重新排序、预插入或返回值过滤。

让我们看一下几个这样的转换。

类型转换

可以使用 asType 方法将方法句柄的类型适配到新类型。如果这种类型转换不可能,我们将得到一个 WrongMethodTypeException。请记住,当我们应用转换时,实际上有两个方法句柄,其中原始方法句柄被包装到一些额外的逻辑中。在这种情况下,包装器将接收参数并尝试将它们转换为与原始方法句柄的参数匹配。一旦原始方法句柄完成其工作并返回结果,包装器将尝试将此结果强制转换为给定类型。

假设我们有一个 test 方法,它接受一个 Object 并返回一个 String。我们可以将这种方法改编为接受更具体的参数类型,例如 String

MethodHandle targetMethodHandle = lookup.findStatic(Example.class, "test", 
        MethodType.methodType(String.class, Object.class));
MethodHandle adapter = targetMethodHandle.asType(
        MethodType.methodType(String.class, String.class));
String originalResult = (String) targetMethodHandle.invoke(111); // works
String adapterResult = (String) adapter.invoke("aaaaaa"); // works
adapterResult = (String) adapter.invoke(111); // fails

事实上,每次我们在 MethodHandle 上使用 invoke 时,首先发生的是一个 asType 调用。invoke 接受和返回 Object,然后尝试将它们转换为更具体的类型。这些特定类型来自我们的代码,即我们作为参数传递的确切值以及我们将返回值强制转换为的类型。一旦类型成功转换,就会为这些特定类型调用 invokeExact 方法。

排列参数

要获得具有重新排序参数的适配器方法句柄,我们可以使用 MethodHandles.permuteArguments

例如,让我们创建一个 test 方法,它接受一堆不同类型的参数

public static void test(int v1, String v2, long v3, boolean v4) {
    System.out.println(v1 + v2 + v3 + v4);
}

并查找指向它的方法句柄

MethodHandle targetMethodHandle = lookup.findStatic(Example.class, "test",
        MethodType.methodType(void.class, int.class, String.class, long.class, boolean.class));

permuteArguments 方法接受

  • 目标方法句柄,在我们的例子中是指向 test 方法的句柄;
  • 新的 MethodType,其中所有参数都以所需的方式重新排序;
  • 一个索引数组,指定参数的新顺序。
MethodHandle reversedArguments = MethodHandles.permuteArguments(targetMethodHandle,
        MethodType.methodType(void.class, boolean.class, long.class, String.class, int.class), 3, 2, 1, 0);
reversedArguments.invoke(false, 1L, "str", 123); // outputs: "123str1false"

插入参数

MethodHandles.insertArguments 方法提供了一个 MethodHandle,其中包含一个或多个绑定参数。

例如,让我们再次看一下前面示例中的方法句柄

MethodHandle targetMethodHandle = lookup.findStatic(Example.class, "test",
        MethodType.methodType(void.class, int.class, String.class, long.class, boolean.class));

我们可以轻松地获得一个具有预先绑定的 Stringlong 参数的适配器 MethodHandle

MethodHandle boundArguments = MethodHandles.insertArguments(targetMethodHandle, 1, "new", 3L);

要调用生成的适配器方法句柄,我们只需要提供未预先填充的参数

boundArguments.invoke(1, true); // outputs: "1new3true"

如果我们尝试传递已经预先填充的参数,我们将遇到 WrongMethodTypeException 错误。

过滤参数

我们可以使用 MethodHandles.filterArguments 在调用目标方法句柄之前对参数应用转换。为了使其工作,我们必须提供

  • 目标方法句柄;
  • 要转换的第一个参数的位置;
  • 每个参数转换的方法句柄。

如果某些参数不需要转换,我们可以通过传递 null 来跳过它们。如果我们只需要转换一部分参数,也可以完全跳过其余参数。

让我们重用上一节中的方法句柄,并在调用之前过滤一些参数。

MethodHandle targetMethodHandle = lookup.findStatic(Example.class, "test",
        MethodType.methodType(void.class, int.class, String.class, long.class, boolean.class));

然后我们创建一个方法,通过取反来转换任何 boolean

private static boolean negate(boolean original) {
    return !original;
}

并构建一个方法,它会增加任何给定的整数值

private static int increment(int original) {
    return ++original;
}

我们可以为这些转换方法获得方法句柄

MethodHandle negate = lookup.findStatic(Example.class, "negate", MethodType.methodType(boolean.class, boolean.class));
MethodHandle increment = lookup.findStatic(Example.class, "increment", MethodType.methodType(int.class, int.class));

并使用它们来获得一个新的方法句柄,该句柄已过滤参数

// applies filter 'increment' to argument at index 0, 'negate' to the last argument, 
// and passes the result to 'targetMethodHandle'
MethodHandle withFilters = MethodHandles.filterArguments(targetMethodHandle, 0, increment, null, null, negate);
withFilters.invoke(3, "abc", 5L, false); // outputs "4abc5true"

折叠参数

当我们想要在调用 MethodHandle 之前对一个或多个参数进行预处理时,我们可以使用 MethodHandles.foldArguments 并为其提供任何组合器方法的方法句柄,该方法将接受从任何首选位置开始的参数。

假设我们有一个 target 方法

private static void target(int ignored, int sum, int a, int b) {
    System.out.printf("%d + %d equals %d and %d is ignored%n", a, b, sum, ignored);
}

使用 foldArguments,我们可以预处理其参数的子集,并将结果值插入为另一个参数,然后继续执行 target 方法。

在我们的示例中,我们在最后有参数 int a, int b。我们可以预处理任意数量的参数,但它们都必须在最后。假设我们想计算这两个值 ab 的总和,所以让我们为此创建一个方法

private static int sum(int a, int b) {
    return a + b;
}

结果值将准确地放在哪里?它将被插入到我们 target 方法的参数之一中。它必须是我们将要折叠的参数之前的参数,所以在我们的示例中是参数 int sum。为折叠结果保留的参数不能位于任何其他位置。如果 target 方法需要接受更多与该折叠逻辑无关的参数,它们都必须放在前面。

让我们创建方法句柄,看看我们应该如何将它们组合在一起

MethodHandle targetMethodHandle = lookup.findStatic(Example.class, "target",
        MethodType.methodType(void.class, int.class, int.class, int.class, int.class));
MethodHandle combinerMethodHandle = lookup.findStatic(Example.class, "sum",
        MethodType.methodType(int.class, int.class, int.class));
MethodHandle preProcessedArguments = MethodHandles.foldArguments(targetMethodHandle, 1, combinerMethodHandle);

foldArguments 方法接受

  • MethodHandle target:目标方法句柄,在我们的例子中是指向 target 方法的句柄。
  • int pos:一个整数,指定与折叠相关的参数的起始位置。在我们的例子中,sum 参数位于位置 1,所以我们传递了 1。如果我们跳过此参数,pos 将默认为 0
  • MethodHandle combiner:组合器方法句柄,在我们的例子中是指向 sum 方法的句柄。

最后,我们可以调用生成的方法句柄,并传递除 sum 之外的所有参数,sum 将被预先计算

preProcessedArguments.invokeExact(10000, 1, 2); // outputs: "1 + 2 equals 3 and 10000 is ignored"

组合器方法可能会处理值,但不会返回任何内容。在这种情况下,target 方法参数列表中不需要结果占位符。

过滤返回值

与参数类似,我们可以使用一个适配器,它将对返回值应用转换。

让我们想象一下,我们有一个返回 String 的方法,我们想将此方法返回的任何值传递到另一个方法,该方法将字符 d 替换为 m 并将结果值大写。

这是 getSomeString 方法的方法句柄,它始终返回 "dummy"

MethodHandle getSomeString = lookup.findStatic(Example.class, "getSomeString", MethodType.methodType(String.class));

这是执行转换的 resultTransform 方法

private static String resultTransform(String value) {
    return value.replace('d', 'm').toUpperCase();
}

这是我们转换器方法的方法句柄

MethodHandle resultTransform = lookup.findStatic(Example.class, "resultTransform", MethodType.methodType(String.class, String.class));

最后,这是两个方法句柄的组合,其中 getSomeString 方法返回的结果随后提供给 resultTransform 方法并相应地修改

MethodHandle getSomeUppercaseString = MethodHandles.filterReturnValue(getSomeString, resultTransform);
System.out.println(getSomeUppercaseString.invoke()); // outputs: "MUMMY"

 

方法句柄与反射 API

方法句柄是在 JDK7 中引入的,作为帮助编译器和语言运行时开发人员的工具。它们从未打算取代反射。

反射 API 提供了方法句柄无法提供的东西,即列出类成员并检查它们的属性。另一方面,方法句柄可以以反射 API 不可能的方式进行转换和操作。

在方法调用方面,存在与访问检查和安全考虑因素相关的差异。反射 API 对每次调用中的每个调用者执行访问检查,而对于方法句柄,访问检查仅在构造期间执行。这使得通过方法句柄进行调用比通过反射更快。但是,必须采取一些预防措施,以防止将方法句柄传递到不应该访问它的代码中。

您可以在 本教程 中了解有关反射的更多信息。

 

反射 API 与方法句柄之间的转换

Lookup 对象可用于将反射 API 对象转换为行为上等效的方法句柄,从而提供对底层类成员的更直接和更高效的访问。

要创建一个指向给定 Method 的方法句柄(假设查找类有权这样做),我们可以使用 unreflect

假设我们在 Example 类中有一个 test 方法,它接受一个 String 参数并返回一个 String。使用反射 API,我们可以获得一个 Method 对象

Method method = Example.class.getMethod("test", String.class);

借助查找对象,我们可以 unreflect Method 对象以获得一个 MethodHandle

MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle methodHandle = lookup.unreflect(method);
String result = (String) methodHandle.invoke("something");

类似地,给定一个 Field 对象,我们可以获得 getter 和 setter 方法句柄

Field field = Example.class.getField("magic");
MethodHandle setterMethodHandle = lookup.unreflectSetter(field);
MethodHandle getterMethodHandle = lookup.unreflectGetter(field);
setterMethodHandle.invoke("something");
String result = (String) getterMethodHandle.invoke(); // result == "something"

MethodHandleMember 的转换也是可能的,条件是未对给定的 MethodHandle 执行任何转换。

假设我们有一个方法句柄,它直接指向一个方法。我们可以使用 MethodHandles.reflectAs 方法来获得 Method 对象

Method method = MethodHandles.reflectAs(Method.class, methodHandle);

它对 Field 对象的工作方式类似

Field field = MethodHandles.reflectAs(Field.class, getterMethodHandle); // same result is achieved by reflecting `setterMethodHandle`

 

结论

在本教程中,我们研究了方法句柄机制,并学习了如何有效地使用它。我们现在知道方法句柄提供了一种高效方法调用方法,但这种机制并不意味着要取代反射 API。

由于采用不同的访问检查方法,方法句柄在方法调用方面具有性能优势。但是,由于访问检查仅在方法句柄创建时执行,因此应谨慎传递方法句柄。

与反射 API 不同,方法句柄不提供任何用于列出类成员和检查其属性的工具。另一方面,方法句柄 API 允许我们将指向方法和字段的直接指针包装到更复杂的逻辑中,例如参数和返回值操作。

最后更新: 2024 年 5 月 31 日


返回教程列表