Java MethodHandles介绍与反射对比区别详解
作者:架构狂魔哥
前言
在本文中,我们将探讨一个重要的API,它是在Java7中引入的,并在以后的jdk版本中得到了增强,即java.lang.invoke.MethodHandles
。
特别是,我们将学习什么是方法句柄(method handles),如何创建它们以及如何使用它们。
什么是方法句柄?
如API文件中所述,关于其定义:
方法句柄是对基础方法、构造函数、字段或类似低级操作的类型化、直接可执行的引用,具有参数或返回值的可选转换。
更简单地说,方法句柄是一种用于查找、调整和调用方法的低级机制。
方法句柄是不可变的,并且没有可见的状态。
要创建和使用MethodHandle,需要4个步骤:
- 创建lookup
- 创建method type
- 查找方法句柄
- 调用方法句柄
方法句柄与反射
引入方法句柄是为了与现有的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,我们必须通过lookup
或publicLookup
对象找到它,同时提供原始类和方法名称。
特别是,查找工厂提供了一组方法,使我们能够在考虑方法范围的情况下以适当的方式找到方法句柄。从最简单的场景开始,让我们探究主要的场景。
方法的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.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个主要主题:
- 查找函数–允许从不同上下文中查找类,并支持接口中的非抽象方法
- 参数处理——改进参数折叠、参数收集和参数传播功能
- 附加组合–添加循环(
loop
、whileLoop
、doWhileLoop
…),并通过tryFinally
提供更好的异常处理支持
这些变化带来了一些额外的好处:
- 增加JVM编译器优化
- 实例化减少
- 在使用MethodHandles API时启用了精度
结论
在本文中,我们介绍了MethodHandles API、它们是什么以及如何使用它们。
我们还讨论了它与反射API的关系,由于方法句柄允许低级别操作,因此最好避免使用它们,除非它们完全适合工作范围。
以上就是Java MethodHandles介绍与反射对比区别详解的详细内容,更多关于Java MethodHandles对比反射的资料请关注脚本之家其它相关文章!