方法句柄简介
此页面由 Nataliia Dziubenko 在 UPL 下贡献什么是方法句柄
方法句柄是一种用于方法查找和调用的低级机制。它们通常与反射进行比较,因为反射 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*/);
有了 Lookup
和 MethodType
实例,我们就可以查找方法句柄。对于实例方法,我们应该使用 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
,唯一的区别是所有参数都可以作为对象数组或列表提供。
此方法的一个有趣特性是,如果提供的参数数量超过预期数量,则所有剩余参数将被压缩到最后一个参数中,该参数将被视为数组。
访问字段
可以创建对字段具有读或写访问权限的方法句柄。对于实例字段,这是通过 findGetter
和 findSetter
方法实现的,对于静态字段,则是通过 findStaticGetter
和 findStaticSetter
方法实现的。我们不需要提供 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
,可以使用 findVarHandle
和 findStaticVarHandle
方法创建。
使用数组
该 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
异常处理
invokeExact
和 invoke
都抛出 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));
我们可以轻松地获得一个具有预先绑定的 String
和 long
参数的适配器 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
。我们可以预处理任意数量的参数,但它们都必须在最后。假设我们想计算这两个值 a
和 b
的总和,所以让我们为此创建一个方法
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"
从 MethodHandle
到 Member
的转换也是可能的,条件是未对给定的 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 日
返回教程列表