封面来源:碧蓝航线 飓风与青春之泉 活动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

使用方法句柄需要四步:

  1. 创建 MethodType 实例;
  2. 创建 MethodHandles.Lookup 实例;
  3. 查找方法句柄;
  4. 调用方法句柄。

简单梳理下:首先确定需要调用的目标方法的参数与返回值,根据这些信息创建出 MethodType 实例,该实例是方法参数与返回值的描述,并不指代某一具体方法。之后创建 Lookup 实例用于查找方法句柄,为了锁定某一具体的方法,在查找时需要传入目标方法所在类的 Class 信息、方法名与 MethodType 实例,最后调用查找到的方法句柄即可。

创建 MethodType 实例

MethodType 描述了方法句柄接收的参数和返回值类型,或者说调用方法句柄时传递的参数和期望的返回值类型。

MethodType 实例是不可变的,每次对其的修改都会产生一个新的 MethodType

创建 MethodType 实例可以使用 MethodType.methodType() 方法,该方法接收一个返回值类型和适量的参数类型,调用方法句柄时,传入的参数类型必须与创建 MethodType 实例时传入的参数类型相匹配。

比如创建返回值和参数类型都为 StringMethodType

1
MethodType methodType = MethodType.methodType(String.class, String.class);

又比如创建无返回值、参数类型为 intMethodType

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();

如果要查找 privateprotected 方法时,则应该这样创建:

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();
// concat(String):返回 String,也接收一个 String
MethodType concatMethodType = MethodType.methodType(String.class, String.class);
// 目标方法在 String 类中,名为 concat
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();
// 由于泛型擦除均使用 Object
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(n)O(n),具体值取决于数据结构的实现方式;
  • Find:通常用于在一组数据中查找满足条件的特定元素,经常使用遍历与比较来实现,这种操作的时间复杂度取决于数据集的大小和查找条件的复杂度,可以是 O(n)O(n) 甚至更高。

3.2 创建 Lookup

在创建一个方法句柄时,要做的第一件事是拿到 Lookup 对象,它是一个工厂对象,用于创建对 Lookup 类可见的方法、构造函数、字段的方法句柄。

利用 MethodHandles 可以创建具有不同访问模式的 Lookup 对象。

比如创建给 public 方法提供访问的 Lookup 对象:

1
MethodHandles.Lookup publicLookup = MethodHandles.publicLookup();

如果还要访问 privateprotected 方法,那么可以:

1
MethodHandles.Lookup lookup = MethodHandles.lookup();

后文将普遍采用第二种方式创建 Lookup 对象。

4. 方法类型

一个类中可以定义很多个方法,就算是同名的方法,也能拥有多个重载。如果要明确创建的方法句柄究竟是哪个方法的引用,那么以下四种信息是必不可少的:

  1. 方法所在的类
  2. 方法名
  3. 方法参数类型
  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
// 返回值类型 int,没有参数
MethodType mt1 = MethodType.methodType(int.class);

如果对应方法没有返回值,其第一个参数使用 void.class

构造返回值类型和参数类型都是 StringMethodType

1
2
// 返回值类型 String,参数类型 String
MethodType mt2 = MethodType.methodType(String.class, String.class);

除此之外,MethodType.methodType() 方法的第二个参数也能接收一个 MethodType 实例,新构造的 MethodType 对象具有与传入的 MethodType 实例相同的参数列表。比如将 mt2 作为参数传入,新得到的 MethodType 对象对应的方法的参数类型是 String

1
2
// 返回值类型 boolean,参数类型 String
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 描述符】的相关内容。

构造返回值类型和参数类型都是 StringMethodType

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));

构造没有返回值,参数类型依次为 intfloatMethodType

1
2
assertThat(MethodType.fromMethodDescriptorString("(IF)V", loader))
.isEqualTo(MethodType.methodType(void.class, int.class, float.class));

也可以构造返回值类型是 int[],参数类型依次为 intStringMethodType

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);
// MethodType 是不变的,每次修改都会产生新的 MethodType,就像 String 一样
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() {
// --snip--

// 一次性修改所有的
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);
// 全部变为 Object 类型
assertThat(unwrapMt.generic()).isEqualTo(MethodType.methodType(Object.class, Object.class, Object.class, Object.class));
// 只引用类型变 Object 类型
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 {
// getName
MethodType getNameMethodType = MethodType.methodType(String.class);
MethodHandle getNameMethodHandle = lookup.findVirtual(Person.class, "getName", getNameMethodType);
// --snip--

// setAge
MethodType setAgeMethodType = MethodType.methodType(void.class, Integer.class);
MethodHandle setAgeMethodHandle = lookup.findVirtual(Person.class, "setAge", setAgeMethodType);
// --snip--
}

获取到方法句柄之后就可以调用了,上述方法句柄引用的方法都是成员方法,成员方法属于某个实例,调用成员方法时需要明确所属的实例对象与调用方法需要传入的参数,调用方法句柄也是如此(和反射类似):

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void testPublicMethod() throws Throwable {
// getName
// --snip--
Person perSon = new Person("test", 18);
String name = (String) getNameMethodHandle.invoke(perSon);
assertThat(name).isEqualTo("test");

// setAge
// --snip--
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 {
// 认为构造函数的返回值是 void
MethodType noArgs = MethodType.methodType(void.class);
// 指定类、MethodType
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 类中存在两个字段 namebool,前者被 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 {
// --snip--

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 {
// --snip--

// 获取静态字段的 MethodHandle
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 {
// --snip--

// 获取私有字段的 Getter
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();

// --snip--
}
1
2
3
4
5
6
7
8
9
@Test
@SneakyThrows
public void testPrivateMethod() {
// --snip--

// 注意权限问题
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() {
// --snip--

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");
}

在实际使用时,应当尽可能明确目标方法实际所在的类,以便让 refcspecialCaller 的值相等。

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");
// 使用 unreflectSpecial
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");
// 调用时传入的参数要与调用 identity() 传入的 Class 对应
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();
// 使用 invoke 成功调用
Object sum = sumMh.invoke(person, Integer.valueOf(1), 2);
assertThat(sum).isEqualTo(3L);

// invokeExact 将更严格地调用,相当于直接调用引用的方法
assertThatExceptionOfType(WrongMethodTypeException.class).isThrownBy(() -> {
// 返回值类型不匹配
Object result = sumMh.invokeExact(person, 1L, 2L);
});
assertThatExceptionOfType(WrongMethodTypeException.class).isThrownBy(() -> {
// 不写返回值也不行,这种情况认为返回值是 void,相当于返回值类型不匹配
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() 方法调用方法句柄时,传入的参数类型是 Integerint,期望的返回值类型是 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 转换为 IntegerInteger 转换为 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();

// 没有返回值的方法给了返回值,返回 null
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 {
// Note: Jumbo argument lists are handled in the variable-arity subclass.
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}));

// invokeExact
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));

// invoke
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));

// invokeWithArguments
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();
// 将最后一个数组类型的参数转换成对应类型的可变参数,invoke 时无需使用原始的数组形式
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]");
// 使用 invokeExact 调用,还是只能用数组
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]]");

// Arrays#asList 默认支持
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);
// 索引 0 位置的元素是个 String 数组,值为 {"1", "2", "3"}
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]");

// 与 asVarargsCollector() 方法类似,只不过 asCollector() 只会收集指定数量的参数
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"));
});

// 数组类型可以是 Object[] 的子类
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);
// 最后三个是 Object -> 最后两个是 String -> 前两个 Object,最后两个 String
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);
});
// 无论那种 invoke 都不接受参数数组
int[] args = {1, 1};
assertThatExceptionOfType(WrongMethodTypeException.class)
.isThrownBy(() -> addExact.invoke(args));
assertThatExceptionOfType(WrongMethodTypeException.class)
.isThrownBy(() -> {
int result = (int) addExact.invokeExact(args);
});

// asSpreader() 将长度可变的参数转换成数组
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);
});
// 使用 invoke 则无所谓
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();
// bindTo 的参数对象必须是 Person 对象或其子类对象
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 {
// --snip--

// 多次绑定
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() 绑定方法参数时, 只能绑定引用类型的参数, 无法绑定 intlong 等基本类型的参数。

针对这种情况,可以先使用 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 {
// --snip--

// 绑定基本类型
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");
// 对于基本类型的绑定需要使用 wrap() 包装下
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() {
// String#substring()
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");
// 在参数 0 位置处添加 float、double 类型的两个参数
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 的位置添加 floatdouble 类型的参数,新得到的方法句柄具有以下参数列表:

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() {
// String#concat
MethodType methodType = MethodType.methodType(String.class, String.class);
MethodHandle concat = lookup.findVirtual(String.class, "concat", methodType);
assertThat(concat.invoke("hello ", "world")).isEqualTo("hello world");
// 设置参数 1 位置处的参数个给定的值
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);
// 接收两个 int,返回最大的 int
MethodHandle max = lookup.findStatic(Math.class, "max", methodType);
// 返回字符串的长度
MethodHandle length = lookup.findVirtual(String.class, "length", MethodType.methodType(int.class));
// 对 max 的索引 1 及其以后的参数使用 length 进行预处理
MethodHandle methodHandle = MethodHandles.filterArguments(max, 0, length, length);
// 虽然传入的 hello world 是字符串,但是会使用 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 个参数作为折叠函数调用时使用的参数,调用折叠函数后得到一个新值,将这个值插入到原始参数列表的首位。

有两点需要注意:

  1. 如果折叠函数的返回值类型是 void,则不会在首位添加参数;
  2. 如果折叠函数的返回值类型不是 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));
// 对传入的参数按照 addExact 得到新值,然后添加到原参数最前面
MethodHandle methodHandle = MethodHandles.foldArguments(getFirst, addExact);
// 1 2 => 3 1 2
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);
// 3 比 4 小,相比较时返回 -1
assertThat(compare.invoke(3, 4)).isEqualTo(-1);
// permute 即改变序列,下述操作将调用时的两个参数交换位置
MethodHandle methodHandle = MethodHandles.permuteArguments(compare, methodType, 1, 0);
// 参数会调换位置,因此相当于 invoke(4, 3),因此返回 1
assertThat(methodHandle.invoke(3, 4)).isEqualTo(1);
// 也可以重复参数
methodHandle = MethodHandles.permuteArguments(compare, methodType, 1, 1);
// 虽然像是 3 与 4 比较,其实是 4 与 4 比较
assertThat(methodHandle.invoke(3, 4)).isEqualTo(0);
}

permuteArguments 共接收三个参数:

  1. 原始方法句柄
  2. 参数重新排列后,新方法句柄的 MethodType
  3. 表示排列顺序(索引)的整数,这些整数的个数要与原始方法句柄的参数个数相同,整数出现的位置和值表示排列的顺序。比如 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);
// 如果传入 parseInt 的参数不能转换为整型数据,则会抛出异常,使用另一个方法句柄处理这个异常
MethodType handleExceptionMt = MethodType.methodType(int.class, Exception.class, String.class);
/*
* 异常处理的方法句柄也有一定的要求:
* 1. 该方法的返回值必须与原方法的返回值一样,第一个参数是处理的异常类型,其他参数依次与原方法对应
* 2. 这里的异常处理方法是成员方法,因此在 invoke 是要首先传入一个对象,而这与原方法的参数列表类型不对应,因此需要使用
* bindTo() 方法,如果异常处理方法也是静态方法,则不存在这个问题。
*/
MethodHandle handleException = lookup.findVirtual(CatchException.class, "handleException", handleExceptionMt)
.bindTo(new CatchException());
MethodHandle methodHandle = MethodHandles.catchException(parseInt, NumberFormatException.class, handleException);
// 控制台还打印出 java
assertThat(methodHandle.invoke("java")).isEqualTo(0);
}

如果异常处理方法是成员方法,在调用成员方法的方法句柄时,第一个参数需要传入实例对象,这与规定的第一个参数是处理的异常类型不符,因此在定义异常处理的方法句柄时要使用 bindTo() 方法绑定异常处理方法所属的对象。

9.7 guardWithTest

guard 意为保护,guardWithTest 表示“带测试的保护”?

guardWithTest 的作用类似 if-else,调用该方法时需要提供三个方法句柄:

  • 第一个方法句柄用于条件判断,因此返回值类型 必须 是基本类型 boolean,不能是包装类型 Boolean

  • 第二个和第三个方法句柄对应的 MethodType 必须一致,当条件判断的方法句柄返回 true 时,后续将调用第二个方法句柄,反之调用第三个。

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);
// 使用第一个方法句柄进行判断,条件满足时调用 max,反之调用 min
MethodHandle test = guardTest.asType(guardTest.type().changeParameterType(0, Integer.class)).bindTo(1);
/*
* guardWithTest() 使用细节:
* 1. 第一个参数的方法句柄必须返回基本类型 boolean,包装类 Boolean 也不行
* 2. 第二个、第三个方法句柄的类型必须一致
* 3. 如果第一个方法句柄对应的方法有参数,则需要使用 bindTo() 方法进行绑定,最终 invoke 传入的参数与
* 第二个、第三个方法句柄的对应的方法参数一致
*/
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);

// substring 执行得到的结果再使用 toLowerCase 执行
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);
// 对 invoker 创建的 MethodHandle 进行变换后,调用时这些变换会自动应用在传入的 MethodHandle 上
assertThat((String) methodHandle.invokeExact(substring, "hello world", 6, 11)).isEqualTo("WORLD");
}

10. 使用方法句柄实现接口

动态代理可以在运行时为接口生成实现类,方法句柄也具备动态实现某个接口的能力。

这需要使用到 java.lang.invoke.MethodHandleProxies#asInterfaceInstance() 方法,该方法接收两个参数:

  1. Class<T> intfc:目标接口的 Class 对象,这个 接口必须是函数式接口
  2. 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);
// 成员方法的 MethodHandle 在调用前需要绑定实例对象
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;
// 不进行权限校验、安全检查的 Lookup
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
lookup,
// 需要包装成的函数式接口方法名
"applyAsInt",
// 函数式接口对应的 MethodType
MethodType.methodType(ToIntFunction.class),
// 函数式接口方法对应的 MethodType
MethodType.methodType(int.class, Object.class),
// 目标方法的 MethodHandle
coder,
// 调用目标方法时需要的 MethodType(返回值、实例、参数...)
MethodType.methodType(byte.class, String.class)
);

@SuppressWarnings("unchecked")
ToIntFunction<String> strCoder = (ToIntFunction<String>) applyAsInt.getTarget().invoke();

assertThat(strCoder.applyAsInt("mofan")).isEqualTo(0);
}