封面来源:碧蓝航线 飓风与青春之泉 活动CG
本文涉及的代码:mofan-demo/api-study/src/test/java/indi/mofan/MethodHandleTest.java
参考链接:
阅读本文之前,请先阅读 Lambda 与序列化 一文作为前置知识
1. 定义与使用场景
1.1 什么是方法句柄
方法句柄由 JDK7 引入,它的 API 文档是这么定义的:
A method handle is a typed, directly executable reference to an underlying method, constructor, field, or similar low-level operation, with optional transformations of arguments or return values.
翻译一下:方法句柄是对底层方法、构造函数、字段或类似的低级操作的有类型的、可直接执行的、并具有参数或返回值的可选转换的引用。
简单来说,方法句柄是一种用于查找、修改和调用方法的底层机制。方法句柄是不可变的,和 String
一样,修改后将产生新的方法句柄,而不是修改原来的方法句柄上,方法句柄也没有可见状态。
通过方法句柄可以直接调用该句柄所引用的底层方法。
关于翻译
Method Handle 译为“方法句柄”,Method 的翻译是没有异议的,Handle 作为动词可以译为处理,作为名词可以译为手柄、把手,因此两者结合后将 Method Handle
译为 方法句柄 ,虽然没有什么错,但难以理解。
“将错就错”,记住一点:Handle
一词在计算机领域中通常指代处理程序或处理函数中的标识符或引用,用于表示对特定资源或事件的处理方式。
这样一解释,Method Handle
指代方法的引用似乎更容易理解?
计算机语言的翻译就是这样,比如 Socket 翻译成“套接字”、Robust 翻译成“鲁棒性”都很莫名其妙,让人茫然,只能说习惯就好。
再说句题外话,如果要命名一个方法表示处理某某的含义,这里的“处理”应该使用 process
,用于表示执行计算和数据处理的硬件或软件组件,而不是使用 handle
。
1.2 使用场景
方法句柄的使用场景主要涉及到 Java 的动态代理和反射技术。比如:
动态代理:方法句柄可以用来创建动态代理对象,在运行时动态地生成一个代理类,并在代理类中对方法的调用进行拦截和处理,实现 AOP。
反射调用:方法句柄可以用于执行反射调用,相比于传统的反射调用,它更高效。
方法调用转发:可以使用方法句柄来实现方法调用转发,对一些类似的方法进行统一的处理。比如将多个方法调用重定向到同一个方法,并在该方法中统一处理这些调用。
Lambda 表达式和方法引用:Java 8 引入了 Lambda 表达式和方法引用,其幕后实现就是方法句柄。在使用 Lambda 表达式或方法引用时,Java 编译器会使用方法句柄来生成对应的字节码。
动态代码生成:方法句柄提供了生成和调用动态代码的能力,在某些动态编程场景下可能有用。
虽然方法句柄提供了强大的动态调用能力,但由于其相对复杂的使用方式和潜在的性能开销,使用方法句柄来调用方法并不是银弹。只有在需要动态生成代码或实现高级的代理和反射功能时才考虑使用方法句柄。
既然方法句柄的使用场景有限,那本文的意义又是什么呢?
此时无用不代表一直无用,等到未来使用到的那一天,曾经的积累就变得弥足珍贵。
1.3 与反射的区别
引入方法句柄是为了与现有的反射 API 一起工作,它们应用在不同的场景,也具有不同的特性。
单从性能角度上看,方法句柄相比于反射更加高效,因为方法句柄的 access checks (访问检查)是在创建时进行的,而不是在运行时。在存在 security manager (安全管理器)时,这种性能差距还会被进一步放大,因为成员和类的查找也需要进行额外的检查。
然而从使用便捷性的角度上看,方法句柄相较于反射更难以使用。尽管如此,方法句柄还是提供了柯里化、更改参数类型和顺序的方式。
2. 使用方式
2.1 Quick Start
使用方法句柄需要四步:
创建 MethodType
实例;
创建 MethodHandles.Lookup
实例;
查找方法句柄;
调用方法句柄。
简单梳理下:首先确定需要调用的目标方法的参数与返回值,根据这些信息创建出 MethodType
实例,该实例是方法参数与返回值的描述,并不指代某一具体方法。之后创建 Lookup
实例用于查找方法句柄,为了锁定某一具体的方法,在查找时需要传入目标方法所在类的 Class
信息、方法名与 MethodType
实例,最后调用查找到的方法句柄即可。
创建 MethodType
实例
MethodType
描述了方法句柄接收的参数和返回值类型,或者说调用方法句柄时传递的参数和期望的返回值类型。
MethodType
实例是不可变的,每次对其的修改都会产生一个新的 MethodType
。
创建 MethodType
实例可以使用 MethodType.methodType()
方法,该方法接收一个返回值类型和适量的参数类型,调用方法句柄时,传入的参数类型必须与创建 MethodType
实例时传入的参数类型相匹配。
比如创建返回值和参数类型都为 String
的 MethodType
:
1 MethodType methodType = MethodType.methodType(String.class, String.class);
又比如创建无返回值、参数类型为 int
的 MethodType
:
1 MethodType methodType = MethodType.methodType(void .class, int .class);
或者创建返回值类型为 List
,参数类型为 Object
数组的 MethodType
:
1 MethodType methodType = MethodType.methodType(List.class, Object[].class);
创建 MethodHandles.Lookup
实例
Lookup
实例通过 MethodHandles
的工厂方法进行创建,能够创建出具有不同访问模式的 Lookup
实例。
创建仅用于查找 public
方法的 Lookup
实例:
1 MethodHandles.Lookup publicLookup = MethodHandles.publicLookup();
如果要查找 private
或 protected
方法时,则应该这样创建:
1 MethodHandles.Lookup lookup = MethodHandles.lookup();
查找方法句柄
定义了 MethodType
,也创建了 Lookup
对象,接下来就是查找方法句柄了。查找方法句柄时需要知道目标方法的所在类和目标方法名称,调用 Lookup
对象的 findVirtual()
方法来查找成员方法的方法句柄。
比如查找 String
对象的 concat()
方法的方法句柄:
1 MethodHandle concat = lookup.findVirtual(String.class, "concat" , concatMethodType);
调用方法句柄
得到方法句柄后,就可以调用方法句柄了,调用时需要传入目标对象(创建的是成员方法的方法句柄)以及调用方法需要的参数信息。
1 String result = (String) concat.invoke("Hello " , "World" );
完整代码
1 2 3 4 5 6 7 8 9 10 11 12 @Test @SneakyThrows public void testQuickStart () { MethodHandles.Lookup lookup = MethodHandles.lookup(); MethodType concatMethodType = MethodType.methodType(String.class, String.class); MethodHandle concat = lookup.findVirtual(String.class, "concat" , concatMethodType); String result = (String) concat.invoke("Hello " , "World" ); assertThat(result).isEqualTo("Hello World" ); }
2.2 执行 Lambda 表达式
利用方法句柄执行目标方法时,需要知道以下信息:
目标方法的返回值类型和参数类型
目标方法的所在类的 Class 信息
目标方法名称
目标方法所在的类的实例对象
执行目标方法所需要的参数
思维发散一下,对于一个 Lambda 表达式能拿到上述信息吗?如果可以,岂不是可以利用方法句柄执行 Lambda 表达式?
答案是肯定的。
1 2 3 4 5 6 7 8 9 10 11 12 @Test @SneakyThrows public void testInvokeLambda () { Function<Integer, Integer> increase = integer -> integer + 1 ; MethodHandles.Lookup lookup = MethodHandles.lookup(); MethodType methodType = MethodType.methodType(Object.class, Object.class); MethodHandle apply = lookup.findVirtual(increase.getClass(), "apply" , methodType); Object result = apply.invoke(increase, 1 ); assertThat(result).isInstanceOf(Integer.class) .isEqualTo(2 ); }
2.3 实现动态代理
在调用方法句柄前后可以增加额外的逻辑,这其实也是动态代理的一种应用,比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 interface Hello { String sayHello () ; } static class HelloImpl implements Hello { @Override public String sayHello () { return "Hello" ; } } @Test @SneakyThrows public void testDynamicProxy () { Hello hello = new HelloImpl (); MethodHandles.Lookup lookup = MethodHandles.lookup(); MethodType methodType = MethodType.methodType(String.class); MethodHandle sayHello = lookup.findVirtual(HelloImpl.class, "sayHello" , methodType); Hello proxy = () -> { String str = null ; try { str = ((String) sayHello.invoke(hello)); } catch (Throwable e) { Assertions.fail(); } return str + " World" ; }; assertThat(proxy.sayHello()).isEqualTo("Hello World" ); }
3. Lookup
3.1 单词 Lookup
Lookup 意为“查阅、查找”,单词 Find 也可以表示“查找”的含义,但它们有一些细微的区别:
Lookup:通常用于在数据结构(哈希表、字典等)中根据 给定的键 来查找对应的值,这种查找方式的时间复杂度可以是常量 O ( 1 ) O(1) O ( 1 ) ,也可以是线性时间复杂度 O ( n ) O(n) O ( n ) ,具体值取决于数据结构的实现方式;
Find:通常用于在一组数据中查找满足条件的特定元素,经常使用遍历与比较来实现,这种操作的时间复杂度取决于数据集的大小和查找条件的复杂度,可以是 O ( n ) O(n) O ( n ) 甚至更高。
3.2 创建 Lookup
在创建一个方法句柄时,要做的第一件事是拿到 Lookup
对象,它是一个工厂对象,用于创建对 Lookup
类可见的方法、构造函数、字段的方法句柄。
利用 MethodHandles
可以创建具有不同访问模式的 Lookup
对象。
比如创建给 public
方法提供访问的 Lookup
对象:
1 MethodHandles.Lookup publicLookup = MethodHandles.publicLookup();
如果还要访问 private
和 protected
方法,那么可以:
1 MethodHandles.Lookup lookup = MethodHandles.lookup();
后文将普遍采用第二种方式创建 Lookup
对象。
4. 方法类型
一个类中可以定义很多个方法,就算是同名的方法,也能拥有多个重载。如果要明确创建的方法句柄究竟是哪个方法的引用,那么以下四种信息是必不可少的:
方法所在的类
方法名
方法参数类型
方法返回值类型
对于第三、第四点可以统称为方法类型,即 MethodType
。
MethodType
确定了一个方法的参数类型和返回值类型,它与某个具体的方法没有直接关系。它表示方法句柄接收的参数和返回的类型,或者方法句柄的调用者传递的参数和期望的返回类型。
与 MethodHandle
一样,MethodType
也是不可变的, 对某个 MethodType
的修改将产生一个新的 MethodType
。
两个方法句柄是否相等,取决于它们包含的参数类型和返回值类型是否一致。 比如,java.lang.String#valueOf(int)
方法和 java.lang.Integer#toString(int)
方法的 MethodType
是相等的,因为这两个方法都接收一个 int
类型的参数并返回 String
类型的值。
4.1 构造方法类型
MethodType
并未提供公共的构造函数,创建 MethodType
只能通过静态工厂完成。
MethodType.methodType()
使用 MethodType.methodType()
构造 MethodType
是最常见的方式。
该方法有多个重载,其 第一个参数总是方法的返回值类型, 后续的参数则表示方法的参数类型。
构造 String#length()
方法的 MethodType
:
1 2 MethodType mt1 = MethodType.methodType(int .class);
如果对应方法没有返回值,其第一个参数使用 void.class
。
构造返回值类型和参数类型都是 String
的 MethodType
:
1 2 MethodType mt2 = MethodType.methodType(String.class, String.class);
除此之外,MethodType.methodType()
方法的第二个参数也能接收一个 MethodType
实例,新构造的 MethodType
对象具有与传入的 MethodType
实例相同的参数列表。比如将 mt2
作为参数传入,新得到的 MethodType
对象对应的方法的参数类型是 String
:
1 2 MethodType mt3 = MethodType.methodType(boolean .class, mt2);
MethodType.genericMethodType()
使用该方法能够生成通用的 MethodType
,即返回值类型和参数类型都是 Object
。
利用这种方式构造的 MethodType
一定 有返回值。
该方法有两种重载。第一种指定参数的个数:
1 2 assertThat(MethodType.genericMethodType(2 )) .isEqualTo(MethodType.methodType(Object.class, Object.class, Object.class));
第二种在第一种的基础上,要求指定参数列表最后是否会有 Object[]
类型的参数:
1 2 assertThat(MethodType.genericMethodType(1 , true )) .isEqualTo(MethodType.methodType(Object.class, Object.class, Object[].class));
MethodType.fromMethodDescriptorString()
该方法与 MethodType.methodType()
类似,只不过要求传入 方法描述符 ,根据传入的方法描述符构造出与之对应的 MethodType
。如果不熟悉方法描述符,可以参考 Lambda 与序列化 一文中【4.4 Java 描述符】的相关内容。
构造返回值类型和参数类型都是 String
的 MethodType
:
1 2 3 ClassLoader loader = Thread.currentThread().getContextClassLoader();assertThat(MethodType.fromMethodDescriptorString("(Ljava/lang/String;)Ljava/lang/String;" , loader)) .isEqualTo(MethodType.methodType(String.class, String.class));
构造没有返回值,参数类型依次为 int
、float
的 MethodType
:
1 2 assertThat(MethodType.fromMethodDescriptorString("(IF)V" , loader)) .isEqualTo(MethodType.methodType(void .class, int .class, float .class));
也可以构造返回值类型是 int[]
,参数类型依次为 int
、String
的 MethodType
:
1 2 assertThat(MethodType.fromMethodDescriptorString("(ILjava/lang/String;)[I" , loader)) .isEqualTo(MethodType.methodType(int [].class, int .class, String.class));
4.2 修改方法类型
在得到 MethodType
实例后,还可以对其进行修改,例如:
增加参数类型
在指定位置增加参数类型
删除指定范围内的参数类型
修改指定位置的参数类型
修改返回值类型
MethodType
是不可变的,对某个 MethodType
的修改将产生一个新的 MethodType
,就像 String
一样。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Test public void testModifyMethodType () { MethodType methodType = MethodType.methodType(String.class, int .class, int .class); MethodType anotherMt = methodType.appendParameterTypes(String.class); assertThat(anotherMt).isNotSameAs(methodType); assertThat(anotherMt).isEqualTo(MethodType.methodType(String.class, int .class, int .class, String.class)); MethodType mt = methodType.insertParameterTypes(1 , float .class, double .class); assertThat(mt).isEqualTo(MethodType.methodType(String.class, int .class, float .class, double .class, int .class)); assertThat(mt.dropParameterTypes(0 , 2 )) .isEqualTo(MethodType.methodType(String.class, double .class, int .class)); assertThat(methodType.changeParameterType(0 , long .class)) .isEqualTo(MethodType.methodType(String.class, long .class, int .class)); assertThat(methodType.changeReturnType(void .class)) .isEqualTo(MethodType.methodType(void .class, int .class, int .class)); }
除此之外,还提供了类似“批量修改”的能力,能够一次性对返回类型和所有参数类型进行修改:
wrap()
:将所有基本类型修改为对应的包装类型
unwrap()
:与 wrap()
相反,将所有包装类型修改为对应的基本类型
generic()
:将所有类型都修改为 Object
类型
erase()
:只将引用类型修改为 Object
类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Test public void testModifyMethodType () { MethodType unwrapMt = MethodType.methodType(int .class, long .class, double .class, String.class); MethodType wrapMt = MethodType.methodType(Integer.class, Long.class, Double.class, String.class); assertThat(unwrapMt.wrap()).isEqualTo(wrapMt); assertThat(wrapMt.unwrap()).isEqualTo(unwrapMt); assertThat(unwrapMt.generic()).isEqualTo(MethodType.methodType(Object.class, Object.class, Object.class, Object.class)); assertThat(unwrapMt.erase()).isEqualTo(MethodType.methodType(int .class, long .class, double .class, Object.class)); }
5. 获取方法句柄
MethodType
相当于是对某个具体方法的抽象,描述了方法的 返回类型 和 参数类型 。
要指向某个具体的方法,除了知道返回类型和参数类型外,还需要知道 目标方法的所在类 与 方法名 ,这两种信息并没有进行分装,而是在构造方法句柄实例时显式传入。
5.1 成员方法
1 2 3 4 5 6 7 8 9 10 11 12 public class Person { private String name; private Integer age; public String getName () { return name; } public void setAge (Integer age) { this .age = age; } }
如果需要获取 getName()
方法和 setAge()
方法对应的方法句柄,那么在拿到它们对应的 MethodType
实例后,使用 Lookup
对象的 findVirtual()
方法来获取:
1 2 3 4 5 6 7 8 9 10 11 12 @Test public void testPublicMethod () throws Throwable { MethodType getNameMethodType = MethodType.methodType(String.class); MethodHandle getNameMethodHandle = lookup.findVirtual(Person.class, "getName" , getNameMethodType); MethodType setAgeMethodType = MethodType.methodType(void .class, Integer.class); MethodHandle setAgeMethodHandle = lookup.findVirtual(Person.class, "setAge" , setAgeMethodType); }
获取到方法句柄之后就可以调用了,上述方法句柄引用的方法都是成员方法,成员方法属于某个实例,调用成员方法时需要明确所属的实例对象与调用方法需要传入的参数,调用方法句柄也是如此(和反射类似):
1 2 3 4 5 6 7 8 9 10 11 12 13 @Test public void testPublicMethod () throws Throwable { Person perSon = new Person ("test" , 18 ); String name = (String) getNameMethodHandle.invoke(perSon); assertThat(name).isEqualTo("test" ); setAgeMethodHandle.invoke(perSon, 100 ); assertThat(perSon).extracting(Person::getAge).isEqualTo(100 ); }
5.2 构造函数
构造函数也有对应的方法句柄,使用 Lookup
实例的 findConstructor()
方法来获取。
与获取成员方法的方法句柄一样,在获取构造函数的方法句柄时,也需要一个 MethodType
实例。
获取 MethodType
实例的方式在前文中已经叙述过,以 MethodType.methodType()
方法为例,该方法的第一个参数总是是方法的返回值类型,那么构造函数的返回值类型是什么呢?
是构造的对象的类型?
不,是 void
。
比如 Person
类中有三种构造函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class Person { private String name; private Integer age; public Person () { } private Person (String name) { this .name = name; } public Person (String name, Integer age) { this .name = name; this .age = age; } }
现在需要获取无参构造函数和全参构造函数对应的方法句柄,那么有:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Test public void testConstructor () throws Throwable { MethodType noArgs = MethodType.methodType(void .class); MethodHandle noArgsMethodHandle = lookup.findConstructor(Person.class, noArgs); Person noArgsPerson = (Person) noArgsMethodHandle.invoke(); assertThat(noArgsPerson).isNotNull(); MethodType allArgs = MethodType.methodType(void .class, String.class, Integer.class); MethodHandle allArgsMethodHandle = lookup.findConstructor(Person.class, allArgs); Person allArgsPerson = (Person) allArgsMethodHandle.invoke("test" , 18 ); assertThat(allArgsPerson).extracting(Person::getName, Person::getAge) .containsExactly("test" , 18 ); }
5.3 静态方法
获取静态方法的方法句柄与获取成员方法的方法句柄很类似,它们之间的区别体现在:
使用 Lookup
实例的 findStatic()
方法来获取静态方法的方法句柄,而不是 findVirtual()
;
成员方法属于某个实例,静态方法则属于类。因此在调用静态方法对应的方法句柄时,无需传入实例对象,直接传入需要的参数即可。
1 2 3 4 5 public class Person { public static Integer returnInt (Integer integer) { return integer; } }
1 2 3 4 5 6 7 @Test public void testPublicStaticMethod () throws Throwable { MethodType returnIntMethodType = MethodType.methodType(Integer.class, Integer.class); MethodHandle returnIntMh = lookup.findStatic(Person.class, "returnInt" , returnIntMethodType); Integer result = (Integer) returnIntMh.invoke(2 ); assertThat(result).isEqualTo(2 ); }
5.4 公共字段
Person
类中存在两个字段 name
和 bool
,前者被 private
修饰,后者被 public
修饰:
1 2 3 4 public class Person { private String name; public Boolean bool; }
通过 findGetter()
方法获取 公共字段 对应的方法句柄:
1 2 3 4 5 6 7 8 @Test public void testPublicField () throws Throwable { MethodHandle getterMh = lookup.findGetter(Person.class, "bool" , Boolean.class); Person person = new Person (); person.setBool(true ); Boolean bool = (Boolean) getterMh.invoke(person); assertThat(bool).isTrue(); }
还可以通过 findSetter()
方法获取到的方法句柄对字段值进行修改:
1 2 3 4 5 6 7 8 9 @Test public void testPublicField () throws Throwable { MethodHandle setterMh = lookup.findSetter(Person.class, "bool" , Boolean.class); person = new Person (); setterMh.invoke(person, false ); assertThat(person.bool).isFalse(); }
对于静态字段也提供了类似的 API,只不过调用方法句柄时,无需传入实例对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 @Test public void testPublicField () throws Throwable { MethodHandle staticGetterMh = lookup.findStaticGetter(Person.class, "CONSTANT" , String.class); assertThat(((String) staticGetterMh.invoke())).isEqualTo("HELLO_WORLD" ); MethodHandle staticSetterMh = lookup.findStaticSetter(Person.class, "CONSTANT" , String.class); staticSetterMh.invoke("GOOD JOB" ); assertThat(Person.CONSTANT).isEqualTo("GOOD JOB" ); Person.CONSTANT = "HELLO_WORLD" ; }
使用以上方式能够获取 私有字段 对应的方法句柄吗?
1 2 3 4 5 6 7 8 @Test public void testPublicField () throws Throwable { assertThatExceptionOfType(IllegalAccessException.class) .isThrownBy(() -> lookup.findGetter(Person.class, "name" , String.class)); }
答案是否定的。当前的 Lookup
实例不能访问 Person
类中的私有字段 name
,最终抛出 IllegalAccessException
。
前文获取的方法句柄对应的方法或字段都是由 public
修饰的,那非 public
方法或字段的方法句柄应该怎么获取呢?
5.5 私有方法
要求获取 MyClass
类内部私有方法 privateMethod()
对应的方法句柄:
1 2 3 4 5 static class MyClass { private String privateMethod (int i) { return String.valueOf(i); } }
获取私有方法的方法句柄时,需要使用到 Lookup#findSpecial()
方法:
1 2 3 4 5 6 @SneakyThrows @CanIgnoreReturnValue private MethodHandle getPrivateMh (MethodHandles.Lookup lookup, MethodType methodType) { return lookup.findSpecial(MyClass.class, "privateMethod" , methodType, MyClass.class); }
尝试一下:
1 2 3 4 5 6 7 8 @Test @SneakyThrows public void testPrivateMethod () { MethodType methodType = MethodType.methodType(String.class, int .class); assertThatExceptionOfType(IllegalAccessException.class) .isThrownBy(() -> getPrivateMh(lookup, methodType)) .withMessageContaining("no private access for invokespecial" ); }
依旧抛出了 IllegalAccessException
异常,这是因为使用的 lookup
对象没有访问 privateMethod()
方法的权限。
和反射一样,方法句柄也有权限问题,只不过与反射在运行时进行权限检查不同,方法句柄的权限检查是在创建阶段完成的,而在实际调用过程中,JVM 并不会检查方法句柄的权限。如果多次调用某个方法句柄,与反射调用相比,方法句柄能够节省下多次权限检查的开销。
方法句柄的访问权限与 Lookup
对象的创建位置有关,与方法句柄的创建位置无关。
在 MyClass
类的内部声明一个 Lookup
实例:
1 2 3 4 5 static class MyClass { public static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup(); }
1 2 3 4 5 6 7 8 9 @Test @SneakyThrows public void testPrivateMethod () { MethodHandle privateMethod = getPrivateMh(MyClass.LOOKUP, methodType); assertThat(privateMethod.invoke(new MyClass (), 212 )).asString().isEqualTo("212" ); }
但问题又来了,如果没法修改源码呢?
在 JDK9 中新增了 MethodHandles.privateLookupIn()
静态方法,该方法可以将一个不具有访问目标类权限的 Lookup
实例模拟成具有相关访问权限的 Lookup
实例:
1 2 3 4 5 6 7 8 9 @Test @SneakyThrows public void testPrivateMethod () { MethodHandles.Lookup privateLookup = MethodHandles.privateLookupIn(MyClass.class, lookup); privateMethod = getPrivateMh(privateLookup, methodType); assertThat(privateMethod.invoke(new MyClass (), 212 )).asString().isEqualTo("212" ); }
findSpecial()
方法相比于 findVirtual()
、findStatic()
多了一个参数,也就是第四个参数 specialCaller
,它用于指定调用方法句柄时实际使用的类,必须具有访问私有方法的权限,限定了查找方法的范围,可以与第一个参数 refc
相等,或者是它的子类。
除此之外,传入的 Lookup
实例指向的类必须和 specialCaller
相等。
因此前文中获取私有字段的方法句柄可以修改成:
1 2 3 4 5 6 7 8 @Test @SneakyThrows public void testPrivateProperty () { MethodHandles.Lookup privateLookup = MethodHandles.privateLookupIn(Person.class, lookup); MethodHandle handle = privateLookup.findGetter(Person.class, "name" , String.class); Person person = new Person ("mofan" , 21 ); assertThat(handle.invoke(person)).isEqualTo("mofan" ); }
还可以这样获取私有构造器的方法句柄:
1 2 3 4 5 6 7 8 9 10 11 @Test @SneakyThrows public void testPrivateConstructor () { MethodHandles.Lookup privateLookup = MethodHandles.privateLookupIn(Person.class, lookup); MethodType methodType = MethodType.methodType(void .class, String.class); MethodHandle constructor = privateLookup.findConstructor(Person.class, methodType); Person person = (Person) constructor.invokeExact("mofan" ); assertThat(person).isNotNull() .extracting(Person::getName) .isEqualTo("mofan" ); }
findSpecial()
的细节
说实话,findSpecial()
的一些细节并不是很好理解,可以参考以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 static class GrandFather { String getStr () { return "GrandFather" ; } } static class Father extends GrandFather { @Override String getStr () { return "Father" ; } } static class Son extends Father {} static class Grandson extends Son { @Override String getStr () { return "GrandSon" ; } } @Test @SneakyThrows public void testFindSpecial () { MethodType methodType = MethodType.methodType(String.class); MethodHandles.Lookup grandsonLookup = MethodHandles.privateLookupIn(Grandson.class, lookup); Grandson grandSon = new Grandson (); MethodHandle getStr = grandsonLookup.findSpecial(Grandson.class, "getStr" , methodType, Grandson.class); assertThat(getStr.invoke(grandSon)).isEqualTo("GrandSon" ); getStr = grandsonLookup.findSpecial(Son.class, "getStr" , methodType, Grandson.class); assertThat(getStr.invoke(grandSon)).isEqualTo("Father" ); getStr = grandsonLookup.findSpecial(Father.class, "getStr" , methodType, Grandson.class); assertThat(getStr.invoke(grandSon)).isEqualTo("Father" ); getStr = grandsonLookup.findSpecial(GrandFather.class, "getStr" , methodType, Grandson.class); assertThat(getStr.invoke(grandSon)).isEqualTo("Father" ); getStr = MethodHandles.privateLookupIn(Father.class, lookup) .findSpecial(GrandFather.class, "getStr" , methodType, Father.class); assertThat(getStr.invoke(grandSon)).isEqualTo("GrandFather" ); }
在实际使用时,应当尽可能明确目标方法实际所在的类,以便让 refc
和 specialCaller
的值相等。
5.6 搭配反射
在使用反射时,会按照需求创建以下实例:
Constructor
:构造器
Method
:方法
Field
:字段
在获取方法句柄时,也能够根据以上实例获取对应的方法句柄:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 @Test public void testUnreflect () throws Throwable { Constructor<Person> constructor = Person.class.getDeclaredConstructor(String.class); constructor.setAccessible(true ); MethodHandle constructorMh = lookup.unreflectConstructor(constructor); assertThat(((Person) constructorMh.invoke("java" ))) .extracting(Person::getName) .isEqualTo("java" ); Method getStr = Person.class.getDeclaredMethod("getStr" , String.class, Integer.class); getStr.setAccessible(true ); MethodHandle getStrMh = lookup.unreflect(getStr); String str = (String) getStrMh.invoke(new Person ("test" , 18 ), "TEST" , 20 ); assertThat(str).isEqualTo("TEST - 20" ); assertThatExceptionOfType(IllegalAccessException.class) .isThrownBy(() -> lookup.unreflectSpecial(getStr, Person.class)) .withMessageStartingWith("no private access for invokespecial" ); MethodHandle getStrMh2 = MethodHandles.privateLookupIn(Person.class, lookup) .unreflectSpecial(getStr, Person.class); str = (String) getStrMh2.invoke(new Person ("test" , 18 ), "TEST" , 20 ); assertThat(str).isEqualTo("TEST - 20" ); Field field = Person.class.getDeclaredField("name" ); field.setAccessible(true ); Person person = new Person (); MethodHandle setNameMh = lookup.unreflectSetter(field); setNameMh.invoke(person, "test" ); MethodHandle getNameMh = lookup.unreflectGetter(field); String name = (String) getNameMh.invoke(person); assertThat(name).isEqualTo("test" ); }
如果方法句柄引用的信息是私有的,那么先调用对应的 setAccessible()
方法设置访问权限即可。
对于 unreflectSpecial()
方法,第二个参数要求传入一个 Class<?>
对象,作为实际调用目标方法的对象的 Class
,要求 Lookup
实例具有访问它的权限,因此需要使用 MethodHandles.privateLookupIn()
对现有的 lookup
对象进行模拟。
5.7 通用的方法句柄
前文中获取的方法句柄都是利用 Lookup
实例检索得到的,除此之外还可以使用 MethodHandles
中的一些工厂方法来获取一些通用的方法句柄。
操作数组元素的方法句柄
利用 MethodHandles
中的 arrayElementGetter()
方法和 arrayElementSetter()
方法可以生成对数组元素进行操作的方法句柄。比如:
1 2 3 4 5 6 7 8 9 10 @Test @SneakyThrows public void testArrayHandle () { int [] arrays = {1 , 2 , 3 , 4 , 5 , 6 }; MethodHandle getter = MethodHandles.arrayElementGetter(int [].class); assertThat(((int ) getter.invoke(arrays, 1 ))).isEqualTo(2 ); MethodHandle setter = MethodHandles.arrayElementSetter(int [].class); setter.invoke(arrays, 1 , 212 ); assertThat(arrays[1 ]).isEqualTo(212 ); }
MethodHandles.identity()
MethodHandles.identity()
方法接收一个 Class<?>
对象,调用时传入的参数的 Class
信息要与创建方法句柄时指定的 Class<?>
相等,最终会将传入的参数值原样返回:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Test @SneakyThrows public void testIdentity () { MethodHandle stringMh = MethodHandles.identity(String.class); assertThat(stringMh.invoke("java" )).isEqualTo("java" ); assertThatExceptionOfType(WrongMethodTypeException.class) .isThrownBy(() -> stringMh.invoke(123 )); MethodHandle personMh = MethodHandles.identity(Person.class); Person java = new Person ("java" , 100 ); assertThat(((Person) personMh.invoke(java))) .extracting(Person::getName, Person::getAge) .containsExactly("java" , 100 ); }
MethodHandles.constant()
MethodHandles.constant()
方法像是对 MethodHandles.identity()
方法的进一步简化,调用时需要传入两个参数:
Class<?> type
:调用方法句柄期望的返回类型
Object value
:调用方法句柄返回的值
在调用方法句柄时,总是返回传入的 value
值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Test @SneakyThrows public void testConstant () { MethodHandle stringMh = MethodHandles.constant(String.class, "java" ); assertThat(stringMh.invoke()).isEqualTo("java" ); Person java = new Person ("java" , 100 ); MethodHandle personMh = MethodHandles.constant(Person.class, java); assertThat(((Person) personMh.invoke())) .extracting(Person::getName, Person::getAge) .containsExactly("java" , 100 ); assertThatExceptionOfType(ClassCastException.class) .isThrownBy(() -> MethodHandles.constant(long .class, "abc" )); assertThatExceptionOfType(ClassCastException.class) .isThrownBy(() -> MethodHandles.constant(String.class, 123 )); MethodHandle mh = MethodHandles.constant(long .class, 123456 ); assertThat(mh.invoke()).isInstanceOf(Long.class).isEqualTo(123456L ); }
初看 MethodHandles.identity()
和 MethodHandles.constant()
感觉有点意义不明,它们的作用有点类似于 null
。如果在某些场景下需要一个方法句柄,但又没有合适的方法句柄,并且不能传入 null
,此时就可以使用这两个方法生成简单无害的方法句柄进行占位。
6. 调用方法句柄
前文在获取到方法句柄后使用了 invoke()
方法来调用方法句柄,但这并不是调用方法句柄的唯一方式,调用方法句柄有以下三种方式:
invokeExact()
invoke()
invokeWithArguments()
6.1 invokeExact
使用 invokeExact()
方法调用方法句柄与直接调用方法句柄引用的底层方法是一样的,因此在调用时 参数类型和返回值类型要严格匹配。
1 2 3 4 5 public class Person { public long sum (int one, long two) { return one + two; } }
获取 Person
类中 sum()
方法对应的方法句柄,并调用它:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 @Test @SuppressWarnings("all") public void testInvokeExact () throws Throwable { MethodType sumMethodType = MethodType.methodType(long .class, int .class, long .class); MethodHandle sumMh = lookup.findVirtual(Person.class, "sum" , sumMethodType); Person person = new Person (); Object sum = sumMh.invoke(person, Integer.valueOf(1 ), 2 ); assertThat(sum).isEqualTo(3L ); assertThatExceptionOfType(WrongMethodTypeException.class).isThrownBy(() -> { Object result = sumMh.invokeExact(person, 1L , 2L ); }); assertThatExceptionOfType(WrongMethodTypeException.class).isThrownBy(() -> { sumMh.invokeExact(person, 1L , 2L ); }); assertThatExceptionOfType(WrongMethodTypeException.class).isThrownBy(() -> { long result = (long ) sumMh.invokeExact(person, Long.valueOf(1L ), 2L ); }); long result = (long ) sumMh.invokeExact(person, 1 , 2L ); assertThat(result).isEqualTo(3L ); }
sum()
方法接收两个基本类型 long
的参数,最终返回 long
类型的值。
使用 invoke()
方法调用方法句柄时,传入的参数类型是 Integer
和 int
,期望的返回值类型是 Object
,方法句柄能被成功调用。
如果按照相同的调用方式使用 invokeExact()
调用方法句柄,则会抛出 WrongMethodTypeException
,因为真正的返回值类型是 long
,而不是 Object
。
如果方法句柄引用的方法有返回值,那么在使用 invokeExact()
调用方法句柄时,一定要 按照真正的返回值类型对返回值进行强转并声明一个变量来接收,如果没有变量接收返回值,会认为方法的返回值类型是 void
,即没有返回值。
使用 invokeExact()
调用方法句柄时,参数类型要严格匹配, 并且不会自动装箱与拆箱。比如要求的参数类型是 long
,但是传入的参数类型是 Long
,依旧会抛出 WrongMethodTypeException
。
6.2 invoke
相比于 invokeExact()
,invoke()
的要求更加松散,使用 invoke()
调用方法句柄时会尝试对参数和返回值进行类型转换。
如果调用方法句柄传入和接收的类型与引用的方法要求传入和接收的类型完全一致时,invoke()
相当于 invokeExact()
;否则会对参数类型和返回值类型进行类型转换,转换成真正需要的类型。如果能够转换成功,那么一切安好,否则抛出相关异常。
这种类型转换是通过 MethodHandle#asType()
方法来完成的,asType()
方法能够把当前方法句柄适配到新的MethodType
上,并生成一个新的方法句柄。
正是因为 invoke()
方法的松散性调用,使得在调用方法句柄时普遍采用 invoke()
方法进行调用。
invoke()
进行类型转换时会对比所有参数类型和返回值类型是否都可以进行转换,只要其中一个转换失败,那么整个转换过程就会失败。
基本转换规则是:
利用多态完成转换,比如将子类转换为父类;
将类型范围较小的基本类型转换为范围更大的基本类型,比如 int
转为 long
;
利用拆箱、装箱完成转换,比如 int
转换为 Integer
,Integer
转换为 int
;
如果引用的方法有返回值,但调用时没有变量来接收,那么返回值会被丢弃;
如果引用的方法没有返回值,但调用时使用引用类型进行接收,最终的返回值会是 null
;
如果引用的方法没有返回值,但调用时使用基本类型进行接收,最终的返回值会是该基本类型的默认值;
1 2 3 4 5 public class Person { public void print (String string) { System.out.println(string); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Test @SneakyThrows @SuppressWarnings("all") public void testInvokeResult () { MethodType methodType = MethodType.methodType(void .class, String.class); MethodHandle print = lookup.findVirtual(Person.class, "print" , methodType); Person person = new Person (); Object result = print.invoke(person, "Hello World" ); assertThat(result).isNull(); boolean intResult = (boolean ) print.invoke(person, "Hello World" ); assertThat(intResult).isFalse(); }
6.3 invokeWithArguments
invokeWithArguments()
与 invoke()
相比又更加松散,可以指定任意个 Object
类型的参数,与 invokeExact()
、invoke()
不同,它的底层实现不是一个本地方法:
1 2 3 4 5 6 7 8 9 public Object invokeWithArguments (Object... arguments) throws Throwable { MethodType invocationType = MethodType.genericMethodType( arguments == null ? 0 : arguments.length ); return invocationType.invokers() .spreadInvoker(0 ) .invokeExact(asType(invocationType), arguments); }
invokeWithArguments()
首先获取传入参数的个数,然后使用 MethodType.genericMethodType()
方法创建出拥有一个 Object
类型返回值和若干个 Object
类型参数的 MethodType
,将这个 MethodType
通过 asType()
转换后得到一个新的方法句柄,最后使用 invokeExact()
调用这个方法句柄。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 @Test @SneakyThrows @SuppressWarnings("all") public void testInvokeWithArguments () { MethodType subtractMethodType = MethodType.methodType(int .class, int .class, int .class); MethodHandle subtractMh = lookup.findVirtual(Person.class, "subtract" , subtractMethodType); Person person = new Person (); Object two = 2 ; Object one = 1 ; List<Object> arguments = new ArrayList <>(); arguments.add(person); arguments.addAll(List.of(new Object []{2 , 1 })); int result = (int ) subtractMh.invokeExact(person, 2 , 1 ); assertThat(result).isEqualTo(1 ); assertThatExceptionOfType(WrongMethodTypeException.class) .isThrownBy(() -> subtractMh.invokeExact(person, two, one)); assertThatExceptionOfType(WrongMethodTypeException.class) .isThrownBy(() -> subtractMh.invokeExact(arguments)); result = (int ) subtractMh.invoke(person, 2 , 1 ); assertThat(result).isEqualTo(1 ); result = (int ) subtractMh.invoke(person, two, one); assertThat(result).isEqualTo(1 ); assertThatExceptionOfType(WrongMethodTypeException.class) .isThrownBy(() -> subtractMh.invoke(arguments)); result = (int ) subtractMh.invokeWithArguments(person, 2 , 1 ); assertThat(result).isEqualTo(1 ); result = (int ) subtractMh.invokeWithArguments(person, two, one); assertThat(result).isEqualTo(1 ); result = (int ) subtractMh.invokeWithArguments(arguments); assertThat(result).isEqualTo(1 ); assertThatExceptionOfType(WrongMethodTypeException.class) .isThrownBy(() -> subtractMh.invokeWithArguments(person, new Object []{2 , 1 })) .withMessage("cannot convert MethodHandle(Person,int,int)int to (Object,Object)Object" ); }
与 invokeExact()
、invoke()
相比,可以通过反射拿到 invokeWithArguments()
方法对应的 Method
对象进行调用,而不会抛出 UnsupportedOperationException
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Test @SneakyThrows public void testInvokeWithReflect () { MethodType subtractMethodType = MethodType.methodType(int .class, int .class, int .class); MethodHandle subtractMh = lookup.findVirtual(Person.class, "subtract" , subtractMethodType); Person person = new Person (); Class<MethodHandle> clazz = MethodHandle.class; Method invoke = clazz.getDeclaredMethod("invoke" , Object[].class); assertThatThrownBy(() -> invoke.invoke(subtractMh, new Object []{new Object []{person, 2 , 1 }})) .hasCauseInstanceOf(UnsupportedOperationException.class); Method invokeWithArguments = clazz.getDeclaredMethod("invokeWithArguments" , Object[].class); int result = (int ) invokeWithArguments.invoke(subtractMh, new Object []{new Object []{person, 2 , 1 }}); assertThat(result).isEqualTo(1 ); }
7. 可变参数的方法句柄
方法句柄引用的方法的参数除了是固定的参数外,还可以是可变参数,可变参数可以看成是一个数组。对于这种情况,方法句柄也提供了相关的处理能力,使得可变参数和数组类型的参数之间能够相互转换。
7.1 asVarargsCollector
MethodHandle#asVarargsCollector()
方法能够 将原始方法句柄中最后一个数组类型的参数转换成对应类型的可变参数, 转换之后会得到一个新的方法句柄。在调用新的方法句柄时,可以使用可变参数的语法,无需使用数组形式(当然使用数组形式也不会错)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 static class Varargs { public void method (Object... objects) { } } @Test @SneakyThrows @SuppressWarnings("all") public void testAsVarargsCollector () { MethodHandle deepToString = lookup.findStatic(Arrays.class, "deepToString" , MethodType.methodType( String.class, Object[].class )); assertThat(deepToString.isVarargsCollector()).isFalse(); MethodHandle mh = deepToString.asVarargsCollector(Object[].class); assertThat(mh.isVarargsCollector()).isTrue(); assertThat(mh.invoke("one" , "two" )).isEqualTo("[one, two]" ); assertThat(mh.invoke(new Object []{"one" , "two" })).isEqualTo("[one, two]" ); assertThat((String) mh.invokeExact(new Object []{"one" , "two" })).isEqualTo("[one, two]" ); assertThatExceptionOfType(WrongMethodTypeException.class) .isThrownBy(() -> { String str = (String) mh.invokeExact("one" , "two" ); }); assertThat(mh.invoke((Object) new Object []{"one" , "two" })).isEqualTo("[[one, two]]" ); MethodHandle asList = lookup.findStatic(Arrays.class, "asList" , MethodType.methodType( List.class, Object[].class )); assertThat(asList.isVarargsCollector()).isTrue(); assertThat(asList.invoke()).asList().isEmpty(); assertThat(asList.invoke("1" )).asList().containsOnly("1" ); String[] args = {"1" , "2" , "3" }; assertThat(asList.invoke(args)).asList().containsExactly("1" , "2" , "3" ); assertThat(asList.invoke((Object[]) args)).asList().containsExactly("1" , "2" , "3" ); List<?> list = (List<?>) asList.invoke((Object) args); assertThat(list).hasSize(1 ).has(HamcrestCondition.matching(Is.is(new String []{"1" , "2" , "3" })), Index.atIndex(0 )); MethodHandle method = lookup.findVirtual(Varargs.class, "method" , MethodType.methodType( void .class, Object[].class )); assertThat(method.isVarargsCollector()).isTrue(); }
7.2 asCollector
MethodHandle#asCollector()
方法与 MethodHandle#asVarargsCollector()
方法很类似,只不过 MethodHandle#asCollector()
只能把指定数量的最后几个参数收集到原始方法句柄对应的方法的数组参数中, 而不是像 MethodHandle#asVarargsCollector()
将最后所有的参数都收集到数组参数中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 @Test @SneakyThrows public void testAsCollector () { MethodType methodType = MethodType.methodType(String.class, Object[].class); MethodHandle deepToString = lookup.findStatic(Arrays.class, "deepToString" , methodType); assertThat((String) deepToString.invokeExact(new Object []{"java" })).isEqualTo("[java]" ); MethodHandle ts1 = deepToString.asCollector(Object[].class, 1 ); assertThat((String) ts1.invokeExact((Object) new Object []{"java" })) .isNotEqualTo("[java]" ) .isEqualTo("[[java]]" ); assertThat((String) ts1.invokeExact((Object) "java" )).isEqualTo("[java]" ); assertThatExceptionOfType(WrongMethodTypeException.class) .isThrownBy(() -> { String str = ((String) ts1.invokeExact("hello" , "world" )); }); MethodHandle ts2 = deepToString.asCollector(String[].class, 2 ); assertThat(ts2.type()).isEqualTo(MethodType.methodType(String.class, String.class, String.class)); assertThat((String) ts2.invokeExact("one" , "two" )).isEqualTo("[one, two]" ); assertThatExceptionOfType(WrongMethodTypeException.class) .isThrownBy(() -> { String str = (String) ts2.invokeExact("one" , "two" , "three" ); }); MethodHandle ts0 = deepToString.asCollector(Object[].class, 0 ); assertThat((String) ts0.invokeExact()).isEqualTo("[]" ); MethodHandle ts22 = deepToString.asCollector(Object[].class, 3 ) .asCollector(String[].class, 2 ); assertThat((String) ts22.invokeExact((Object) "A" , (Object) "B" , "C" , "D" )) .isEqualTo("[A, B, [C, D]]" ); MethodHandle byteToString = lookup.findStatic( Arrays.class, "toString" , MethodType.methodType(String.class, byte [].class) ).asCollector(byte [].class, 3 ); assertThat((String) byteToString.invokeExact((byte ) 1 , (byte ) 2 , (byte ) 3 )).isEqualTo("[1, 2, 3]" ); MethodHandle longToString = lookup.findStatic( Arrays.class, "toString" , MethodType.methodType(String.class, long [].class) ).asCollector(long [].class, 1 ); assertThat((String) longToString.invokeExact((long ) 212 )).isEqualTo("[212]" ); }
7.3 asSpreader
MethodHandle#asSpreader()
方法则是与 asVarargsCollector()
和 asCollector()
相反,它能够 将可变参数转换成数组类型的参数。 在调用转换后得到的方法句柄时,使用数组作为参数,会按照顺序将数组中的每项依次分配给原始方法句柄中的各个参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 @Test @SneakyThrows @SuppressWarnings("all") public void testAsSpreader () { MethodType addExactMethodType = MethodType.methodType(int .class, int .class, int .class); MethodHandle addExact = lookup.findStatic(Math.class, "addExact" , addExactMethodType); assertThatNoException().isThrownBy(() -> addExact.invoke(1 , 1 )); assertThatNoException().isThrownBy(() -> { int result = (int ) addExact.invokeExact(1 , 1 ); }); int [] args = {1 , 1 }; assertThatExceptionOfType(WrongMethodTypeException.class) .isThrownBy(() -> addExact.invoke(args)); assertThatExceptionOfType(WrongMethodTypeException.class) .isThrownBy(() -> { int result = (int ) addExact.invokeExact(args); }); MethodHandle addExactAsSpreader = addExact.asSpreader(int [].class, 2 ); assertThat(addExactAsSpreader.invoke(args)).isEqualTo(2 ); assertThat((int ) addExactAsSpreader.invokeExact(args)).isEqualTo(2 ); Object arguments = new int []{1 , 1 }; assertThatExceptionOfType(WrongMethodTypeException.class) .isThrownBy(() -> { int result = (int ) addExactAsSpreader.invokeExact(arguments); }); assertThat(addExactAsSpreader.invoke(arguments)).isEqualTo(2 ); MethodType equalsMethodType = MethodType.methodType(boolean .class, Object.class); MethodHandle equals = lookup.findVirtual(String.class, "equals" , equalsMethodType); MethodHandle methodHandle = equals.asSpreader(Object[].class, 2 ); assertThat(methodHandle.invoke(new Object []{"java" , "java" })) .asInstanceOf(InstanceOfAssertFactories.BOOLEAN) .isTrue(); }
7.4 asFixedArity
MethodHandle#asFixedArity()
方法可以 将接收可变参数的方法转换成接收长度不变的方法, 因此在调用转换后得到的方法句柄时, 只能使用传入数组。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Test @SneakyThrows @SuppressWarnings("unchecked, rawtypes") public void testAsFixedArity () { MethodType methodType = MethodType.methodType(List.class, Object[].class); MethodHandle asList = lookup.findStatic(Arrays.class, "asList" , methodType); assertThat(asList.invoke(1 , 2 , 3 )).asList().containsExactly(1 , 2 , 3 ); MethodHandle asListFix = asList.asFixedArity(); assertThatExceptionOfType(WrongMethodTypeException.class) .isThrownBy(() -> asListFix.invoke(1 , 2 , 3 )); Object[] args = {1 , 2 , 3 }; assertThat(asListFix.invoke(args)).asList().containsExactly(1 , 2 , 3 ); List<?> list = (List<?>) asList.invoke((Object) args); assertThat(list).hasSize(1 ).is(HamcrestCondition.matching(Is.is(new int []{1 , 2 , 3 })), Index.atIndex(0 )); list = ((List<?>) asListFix.invoke((Object) args)); assertThat(list).hasSize(3 ).containsExactlyElementsOf((List) List.of(1 , 2 , 3 )); }
8. 参数绑定
在调用方法句柄之前,可以事先对参数进行绑定,后续调用方法句柄时,无需传入绑定的参数,传入剩余所需的参数即可,参数绑定可以使用 MethodHandle#bindTo()
完成。
1 2 3 4 5 6 7 8 9 10 11 12 13 public class Person { private String name; public String getName () { return name; } public void setName (String name) { this .name = name; } }
获取 setName()
方法对应的方法句柄,由于 setName()
方法是成员方法,因此在调用方法句柄时需要传入 Person
实例对象。
如果想要在调用方法句柄时只传入 setName()
方法实际所需的参数,那么可以使用 bindTo()
方法预先绑定 Person
实例对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Test public void testBindTo () throws Throwable { MethodType setNameMethodType = MethodType.methodType(void .class, String.class); MethodHandle setNameMh = lookup.findVirtual(Person.class, "setName" , setNameMethodType); Student student = new Student (); MethodHandle methodHandle = setNameMh.bindTo(student); methodHandle.invoke("test" ); assertThat(student).extracting(Student::getName).isEqualTo("test" ); MethodType concatMethodType = MethodType.methodType(String.class, String.class); MethodHandle concat = lookup.findVirtual(String.class, "concat" , concatMethodType); methodHandle = concat.bindTo("hello " ); assertThat(methodHandle.invoke("world" )).isEqualTo("hello world" ); }
预先绑定参数的方式使得开发者能够只公开方法,而不公开该方法所在的对象,方法句柄的调用者只需关注方法本身,并可以在任何地方直接调用方法句柄,就像直接调用方法一样。
可以多次使用 bindTo()
来绑定多个参数,如果所需的参数都被绑定完了,那在调用时无需传入任何参数:
1 2 3 4 5 6 7 8 9 10 11 @Test public void testBindTo () throws Throwable { MethodType indexOfMethodType = MethodType.methodType(int .class, String.class); MethodHandle indexOf = lookup.findVirtual(String.class, "indexOf" , indexOfMethodType) .bindTo("hello world" ).bindTo("e" ); assertThat(indexOf.invoke()).isEqualTo(1 ); }
使用 bindTo()
绑定方法参数时, 只能绑定引用类型的参数, 无法绑定 int
、long
等基本类型的参数。
针对这种情况,可以先使用 MethodType#wrap()
方法将包含基本类型的 MethodType
转换成对应包装类型的 MethodType
,之后使用 MethodHandle#asType()
方法将原方法句柄转换成使用新 MethodType
的方法句柄。对于新方法句柄,就可以使用 bindTo()
方法进行绑定了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Test public void testBindTo () throws Throwable { MethodType substringMethodType = MethodType.methodType(String.class, int .class, int .class); MethodHandle substring = lookup.findVirtual(String.class, "substring" , substringMethodType); assertThatExceptionOfType(IllegalArgumentException.class) .isThrownBy(() -> substring.bindTo("java" ).bindTo(2 ).bindTo(3 )) .withMessage("no leading reference parameter" ); MethodHandle mh = substring.asType(substring.type().wrap()) .bindTo("java" ).bindTo(2 ).bindTo(3 ); assertThat(mh.invoke()).isEqualTo("v" ); }
9. 变换方法句柄
方法句柄与反射 API 相比,能够进行多种变换,包括对参数和返回值的处理,并且还能够将这些变换进行组合,形成更加复杂的变换。
方法句柄的变换都是由 MethodHandles
类中的静态方法完成,这些静态方法一般都将接收一个方法句柄,最终返回一个变换后的、新的方法句柄。
9.1 dropArguments
drop
意为落下、丢下,dropArguments
的含义并不是舍弃当前方法句柄中的一些参数,而是能够在当前方法句柄中添加一些无用的参数,实际调用时这些参数会被舍弃。
利用 dropArguments
能够使得变换后的方法句柄的参数类型格式符合某些特定的要求。
1 2 3 4 5 6 7 8 9 10 11 12 @Test @SneakyThrows public void testDropArguments () { MethodType methodType = MethodType.methodType(String.class, int .class, int .class); MethodHandle substring = lookup.findVirtual(String.class, "substring" , methodType); assertThat(substring.invoke("hello world" , 6 , 11 )).isEqualTo("world" ); MethodHandle newMh = MethodHandles.dropArguments(substring, 0 , float .class, double .class); assertThat(newMh.invoke(0.5f , 2.33 , "hello world" , 6 , 11 )).isEqualTo("world" ); }
String#substring()
方法接收两个 int
类型的参数,使用 dropArguments
在参数列表索引为 0 的位置添加 float
和 double
类型的参数,新得到的方法句柄具有以下参数列表:
1 float, double, String, int, int
中间的 String
是因为 String#substring()
方法是成员方法,调用时需要传入对应的对象。
9.2 insertArguments
insertArguments
顾名思义就是在当前的方法句柄中插入一些参数,插入的参数会在调用方法句柄时从指定位置依次填充,而无需再次传入。
insertArguments
的作用和 MethodHandle#bindTo()
类似,但它更加强大,能够从指定位置开始,依次绑定多个参数。
1 2 3 4 5 6 7 8 9 10 11 12 @Test @SneakyThrows public void testInsertArguments () { MethodType methodType = MethodType.methodType(String.class, String.class); MethodHandle concat = lookup.findVirtual(String.class, "concat" , methodType); assertThat(concat.invoke("hello " , "world" )).isEqualTo("hello world" ); MethodHandle newMh = MethodHandles.insertArguments(concat, 1 , "!" ); assertThat(newMh.invoke("hello world" )).isEqualTo("hello world!" ); }
String#concat()
是成员方法,并接收一个参数,因此在执行方法句柄时需要传入具体的实例对象和调用方法所需的一个参数,总共两个参数。
使用 insertArguments
在索引 1 位置插入了一个参数,那么在执行时只需要传入索引 0 位置的参数即可。
9.3 filterArguments
使用 filterArguments
能够对当前方法句柄的参数使用另一个方法句柄进行预处理,使得预处理后的参数满足当前方法句柄的调用要求。
其用法和前面的方法类似,传入原始的方法句柄是毋庸置疑的,然后再传入预处理参数的起始索引,最后传入进行预处理的方法句柄,从起始索引开始,依次使用传入的方法句柄对这些参数进行预处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 @Test @SneakyThrows public void testFilterArguments () { MethodType methodType = MethodType.methodType(int .class, int .class, int .class); MethodHandle max = lookup.findStatic(Math.class, "max" , methodType); MethodHandle length = lookup.findVirtual(String.class, "length" , MethodType.methodType(int .class)); MethodHandle methodHandle = MethodHandles.filterArguments(max, 0 , length, length); assertThat(methodHandle.invoke("a" , "hello world" )).isEqualTo(11 ); }
调用 Math.max()
方法需要传入两个 int
类型的参数,但实际调用时传入的是两个 String
类型的参数。
在调用前,使用 filterArguments
对原始方法句柄的参数进行预处理,从索引 0 位置的参数开始,依次使用两个名为 length
的方法句柄进行处理。
你以为传入的是两个 String
,其实传入的是它们的 length
。
9.4 foldArguments
foldArguments
意为“折叠参数”,它能够将执行方法句柄时传入的参数“折叠”为一个新值放在原始参数列表的 首位 ,作为一个新的参数。
“折叠”的逻辑由另一个方法句柄(假设称之为“折叠函数”,我瞎编的 👻)完成。在进行转换时,根据折叠函数所需的参数个数 N,选取实际调用时传入的前 N 个参数,将这前 N 个参数作为折叠函数调用时使用的参数,调用折叠函数后得到一个新值,将这个值插入到原始参数列表的首位。
有两点需要注意:
如果折叠函数的返回值类型是 void
,则不会在首位添加参数;
如果折叠函数的返回值类型不是 void
,那么要求这个类型与调用原始方法句柄时传入的第一个参数类型匹配。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 static class FoldArgument { public static int getFirst (int a, int b, int c) { return a; } } @Test @SneakyThrows public void testFoldArguments () { MethodType methodType = MethodType.methodType(int .class, int .class, int .class); MethodHandle addExact = lookup.findStatic(Math.class, "addExact" , methodType); MethodHandle getFirst = lookup.findStatic(FoldArgument.class, "getFirst" , MethodType.methodType(int .class, int .class, int .class, int .class)); MethodHandle methodHandle = MethodHandles.foldArguments(getFirst, addExact); assertThat(methodHandle.invoke(1 , 2 )).isEqualTo(3 ); }
对转换后的方法句柄 methodHandle
进行调用时,先利用方法句柄 addExact
对传入的参数进行预处理,然后将预处理结果放在原始参数列表的首位,最终使用新的参数列表调用方法句柄 getFirst
。
9.5 permuteArguments
permute
意为交换、置换,常用于表示改变顺序。permuteArguments
用于对原始方法句柄中的参数进行排列,这种排列可以是:
对所有参数重新排列
对部分参数进行排列
忽略某些参数
重复某些参数
以上操作只改变参数的排列,不能改变参数个数。除此之外,还要求新旧方法句柄的返回类型必须一样。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Test @SneakyThrows public void testPermuteArguments () { MethodType methodType = MethodType.methodType(int .class, int .class, int .class); MethodHandle compare = lookup.findStatic(Integer.class, "compare" , methodType); assertThat(compare.invoke(3 , 4 )).isEqualTo(-1 ); MethodHandle methodHandle = MethodHandles.permuteArguments(compare, methodType, 1 , 0 ); assertThat(methodHandle.invoke(3 , 4 )).isEqualTo(1 ); methodHandle = MethodHandles.permuteArguments(compare, methodType, 1 , 1 ); assertThat(methodHandle.invoke(3 , 4 )).isEqualTo(0 ); }
permuteArguments
共接收三个参数:
原始方法句柄
参数重新排列后,新方法句柄的 MethodType
表示排列顺序(索引)的整数,这些整数的个数要与原始方法句柄的参数个数相同,整数出现的位置和值表示排列的顺序。比如 permuteArguments(compare, methodType, 1, 0)
表示调用原始方法句柄的第一个参数是调用新方法句柄的第二个参数
对一个方法句柄进行 permuteArguments
能得到一个新的方法句柄,在调用新的方法句柄时,按照 permuteArguments
时给定的排列将传入的参数映射到原始方法句柄上,最终执行的还是原始方法句柄引用的方法。
9.6 catchExceptions
使用该方法可以为原始方法句柄指定异常处理的方法句柄。如果原始方法句柄正常调用完成,直接返回调用结果;如果执行过程中抛出了异常,那么进行异常处理的方法句柄就会被调用。
进行异常处理的方法句柄并不是随意指定的:
方法句柄的返回值必须和原始方法句柄的返回值相同,在出现异常后,返回值会作为调用的结果;
方法句柄的第一个参数类型必须是所处理的异常类型(或父类),剩余参数与原始方法句柄参数相同。在出现异常后,也能够拿到调用原始方法句柄所使用的参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 static class CatchException { public int handleException (Exception e, String str) { System.out.println(str); return 0 ; } } @Test @SneakyThrows public void testCatchExceptions () { MethodType methodType = MethodType.methodType(int .class, String.class); MethodHandle parseInt = lookup.findStatic(Integer.class, "parseInt" , methodType); assertThat(parseInt.invoke("212" )).isEqualTo(212 ); MethodType handleExceptionMt = MethodType.methodType(int .class, Exception.class, String.class); MethodHandle handleException = lookup.findVirtual(CatchException.class, "handleException" , handleExceptionMt) .bindTo(new CatchException ()); MethodHandle methodHandle = MethodHandles.catchException(parseInt, NumberFormatException.class, handleException); assertThat(methodHandle.invoke("java" )).isEqualTo(0 ); }
如果异常处理方法是成员方法,在调用成员方法的方法句柄时,第一个参数需要传入实例对象,这与规定的第一个参数是处理的异常类型不符,因此在定义异常处理的方法句柄时要使用 bindTo()
方法绑定异常处理方法所属的对象。
9.7 guardWithTest
guard
意为保护,guardWithTest
表示“带测试的保护”?
guardWithTest
的作用类似 if-else
,调用该方法时需要提供三个方法句柄:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Test @SneakyThrows public void testGuardWithTest () { MethodHandle guardTest = lookup.findStatic(GuardWithTest.class, "guardTest" , MethodType.methodType(boolean .class, int .class)); MethodType methodType = MethodType.methodType(int .class, int .class, int .class); MethodHandle max = lookup.findStatic(Math.class, "max" , methodType); MethodHandle min = lookup.findStatic(Math.class, "min" , methodType); MethodHandle test = guardTest.asType(guardTest.type().changeParameterType(0 , Integer.class)).bindTo(1 ); assertThat(MethodHandles.guardWithTest(test, max, min).invoke(1 , 2 )) .isEqualTo(1 ); }
调用新生成的方法句柄时,传入的参数应该与第二个(或第三个,无所谓,反正它们都是一样的)方法句柄所需的参数一致。
如果用于条件判断的方法句柄也需要接收参数,应该提前使用 bindTo()
方法完成参数的绑定。
9.8 filterReturnValue
前面几种方法都是对方法句柄的参数的变换,那返回值能进行变换吗?
filterReturnValue
接收两个方法句柄,在调用新生成的方法句柄时,传入的参数应当与第一个方法句柄所需的参数一致, 之后使用这些参数来调用第一个方法句柄,返回的结果会作为参数传递给第二个方法句柄并完成最终的调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 @Test @SneakyThrows public void testFilterReturnValue () { MethodType substringMt = MethodType.methodType(String.class, int .class, int .class); MethodHandle substring = lookup.findVirtual(String.class, "substring" , substringMt); MethodType toLowerCaseMt = MethodType.methodType(String.class); MethodHandle toLowerCase = lookup.findVirtual(String.class, "toUpperCase" , toLowerCaseMt); MethodHandle methodHandle = MethodHandles.filterReturnValue(substring, toLowerCase); assertThat(methodHandle.invoke("hello world" , 6 , 11 )).isEqualTo("WORLD" ); }
9.9 invoker
如果需要对多个方法句柄进行相同的变换,除了多次进行相同的变换外,还可以创建一个用来调用其他方法句柄的方法句柄(这样的方法句柄称为“元方法句柄”), 对元方法句柄的变换会自动应用到元方法句柄调用的方法句柄上。
创建元方法句柄的方式有两种:
MethodHandles.invoker(MethodType)
MethodHandles.exactInvoker(MethodType)
它们都接收一个 MethodType
类型的参数。
对于 invoker()
来说,它创建出的元方法句柄,等价于使用以下方式创建出的方法句柄:
1 MethodHandles.publicLookup().findVirtual(MethodHandle.class, "invoke" , type)
那么 exactInvoker()
就等价于:
1 MethodHandles.publicLookup().findVirtual(MethodHandle.class, "invokeExact" , type)
这里的 type
是创建元方法句柄时传入的 MethodType
对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Test @SneakyThrows public void testExactInvoker () { MethodType typeInvoker = MethodType.methodType(String.class, String.class, int .class, int .class); MethodHandle invoker = MethodHandles.exactInvoker(typeInvoker); MethodType typeFind = MethodType.methodType(String.class, int .class, int .class); MethodHandle substring = lookup.findVirtual(String.class, "substring" , typeFind); assertThat(invoker.invoke(substring, "hello world" , 6 , 11 )) .isEqualTo(substring.invoke("hello world" , 6 , 11 )) .isEqualTo("world" ); MethodHandle toUpperCase = lookup.findVirtual( String.class, "toUpperCase" , MethodType.methodType(String.class) ); MethodHandle methodHandle = MethodHandles.filterReturnValue(invoker, toUpperCase); assertThat((String) methodHandle.invokeExact(substring, "hello world" , 6 , 11 )).isEqualTo("WORLD" ); }
10. 使用方法句柄实现接口
动态代理可以在运行时为接口生成实现类,方法句柄也具备动态实现某个接口的能力。
这需要使用到 java.lang.invoke.MethodHandleProxies#asInterfaceInstance()
方法,该方法接收两个参数:
Class<T> intfc
:目标接口的 Class
对象,这个 接口必须是函数式接口
MethodHandle target
:目标接口中唯一抽象方法对应的 MethodHandle
1 2 3 4 5 6 7 8 9 10 11 @Test @SneakyThrows @SuppressWarnings("unchecked") public void testAsInterfaceInstance () { MethodType methodType = MethodType.methodType(String.class, Integer.class); MethodHandle convert = lookup.findVirtual(UseMethodHandleProxies.class, "convert" , methodType); convert = convert.bindTo(new UseMethodHandleProxies ()); Function<Integer, String> function = MethodHandleProxies.asInterfaceInstance(Function.class, convert); assertThat(function.apply(3 )).isEqualTo("4" ); }
也就说,通过 asInterfaceInstance()
方法能够将任意方法转换成一个函数式接口。
除此之外,还有一种更复杂的方式实现将任意方法转换成一个函数式接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 @Test @SneakyThrows public void testLambdaMetafactory () { Class<MethodHandles.Lookup> lookupClass = MethodHandles.Lookup.class; Field implLookup = lookupClass.getDeclaredField("IMPL_LOOKUP" ); implLookup.setAccessible(true ); MethodHandles.Lookup lookup = (MethodHandles.Lookup) implLookup.get(null ); MethodType coderMt = MethodType.methodType(byte .class); MethodHandle coder = lookup.in(String.class).findSpecial(String.class, "coder" , coderMt, String.class); CallSite applyAsInt = LambdaMetafactory.metafactory( lookup, "applyAsInt" , MethodType.methodType(ToIntFunction.class), MethodType.methodType(int .class, Object.class), coder, MethodType.methodType(byte .class, String.class) ); @SuppressWarnings("unchecked") ToIntFunction<String> strCoder = (ToIntFunction<String>) applyAsInt.getTarget().invoke(); assertThat(strCoder.applyAsInt("mofan" )).isEqualTo(0 ); }