Java反射与MethodHandle均是实现运行时动态方法调用的核心技术,但两者在设计目标与底层实现上存在显著差异,这直接影响了它们的性能表现。
- 反射 (Reflection):通过
Method对象封装方法的元数据,提供了统一的调用接口。但其每次调用均需进行运行时的访问安全检查,导致额外的性能开销,且无法直接操作参数的类型或顺序。
- MethodHandle:通过预编译的句柄(例如
MethodHandle.invokeExact)将安全检查提前至句柄创建阶段。调用时可直接跳转至目标方法,性能接近直接调用,尤其适用于高频调用的高性能场景。
本文将深入介绍MethodHandle的详细使用方法,并通过基准测试直观对比其与反射在动态调用场景下的性能差异。
实战案例:MethodHandle使用四步法
创建并使用一个MethodHandle通常包含以下四个步骤:
- 创建查找对象(
Lookup)
- 创建方法类型(
MethodType)
- 查找方法句柄(
MethodHandle)
- 调用方法句柄(
invoke)
1. 创建Lookup对象
Lookup是一个工厂对象,用于为查找类中可见的方法、构造函数和字段创建方法句柄。可以通过MethodHandles类创建不同访问权限的Lookup对象。
// 创建仅能访问公共方法的查找器
MethodHandles.Lookup publicLookup = MethodHandles.publicLookup();
// 如需访问私有或受保护方法,需使用 lookup() 方法
MethodHandles.Lookup lookup = MethodHandles.lookup();
2. 创建方法类型MethodType
MethodType用于描述方法句柄所接受参数的类型以及返回类型,它由一个返回类型和一系列参数类型组成。MethodType实例是不可变的。
创建一个返回类型为BigDecimal,参数类型为double的MethodType:
MethodType mt = MethodType.methodType(BigDecimal.class, double.class);
创建一个返回类型为void,参数类型为String的MethodType:
MethodType mt = MethodType.methodType(void.class, String.class);
注意:若方法返回原始类型或void,需使用对应的类字面量(如void.class, int.class)。
3. 查找方法句柄MethodHandle
在获得MethodType对象后,即可通过Lookup对象查找具体的方法句柄。
查找实例方法
使用findVirtual()方法,例如查找String类的concat方法:
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodType mt = MethodType.methodType(String.class, String.class);
MethodHandle concatMethodHandle = lookup.findVirtual(String.class, "concat", mt);
查找静态方法
使用findStatic()方法,例如查找Arrays.asList方法:
MethodType mt = MethodType.methodType(List.class, Object[].class);
MethodHandle asListMethodHandle = lookup.findStatic(Arrays.class, "asList", mt);
查找构造函数
使用findConstructor()方法,例如查找String类的有参构造函数:
MethodType mt = MethodType.methodType(void.class, String.class);
MethodHandle newStringMethodHandle = lookup.findConstructor(String.class, mt);
访问私有方法
访问私有方法需先通过反射API获取Method对象,再由Lookup进行包装:
public class Book {
private BigDecimal calcDiscount(double discount) {
return this.price.multiply(BigDecimal.valueOf(discount)).setScale(2, RoundingMode.HALF_UP);
}
}
Method calcDiscountMethod = Book.class.getDeclaredMethod("calcDiscount", double.class);
calcDiscountMethod.setAccessible(true); // 必须设置为可访问
MethodHandle calcDiscountMethodHandle = lookup.unreflect(calcDiscountMethod);
4. MethodHandle的调用方式
获得MethodHandle后,有三种主要的调用方式。
invoke 方法调用
此方法要求参数数量固定,但允许对参数和返回类型进行强制转换、装箱/拆箱。
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodType mt = MethodType.methodType(String.class, char.class, char.class);
MethodHandle replaceMH = lookup.findVirtual(String.class, "replace", mt);
String ret = (String) replaceMH.invoke("pacg", Character.valueOf('g'), 'k');
System.err.println(ret); // 输出:pack
invokeWithArguments 方法调用
这是最宽松的调用方式,支持可变参数,并允许参数类型的强制转换和装箱/拆箱。
Book book = new Book();
book.setPrice(BigDecimal.valueOf(70D));
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodType mt = MethodType.methodType(BigDecimal.class, double.class);
MethodHandle calcDiscountMethodHandle = lookup.findVirtual(Book.class, "calcDiscount", mt);
Object ret = calcDiscountMethodHandle.invokeWithArguments(book, 0.55D);
System.err.println(ret); // 输出:38.50
invokeExact 方法调用
这是最严格的调用方式,要求参数数量和类型必须精确匹配,不执行任何自动转换。
MethodType mt = MethodType.methodType(BigDecimal.class, double.class);
MethodHandle calcDiscountMethodHandle = lookup.findVirtual(Book.class, "calcDiscount", mt);
BigDecimal r = (BigDecimal) calcDiscountMethodHandle.invokeExact(book, 0.55D);
注意:使用invokeExact时,返回值必须进行精确的类型转换,否则会抛出WrongMethodTypeException。
MethodHandle高级用法
数组参数展开
通过asSpreader方法,可以将方法句柄适配为接收一个数组参数,从而“展开”数组中的元素作为多个独立参数。这在处理可变参数方法时非常有用。
public class Book {
public boolean isPriceEqual(Book other) {
if (other == null) return false;
return this.price.compareTo(other.price) == 0;
}
}
Book book1 = new Book("Spring Boot3实战案例200讲", "pack_xg", BigDecimal.valueOf(70D));
Book book2 = new Book("Spring全家桶实战案例", "pack_xg", BigDecimal.valueOf(60D));
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodType mt = MethodType.methodType(boolean.class, Book.class);
MethodHandle isPriceEqualMethodHandle = lookup.findVirtual(Book.class, "isPriceEqual", mt);
// 将句柄适配为接收一个Book[]参数,并将其展开
isPriceEqualMethodHandle = isPriceEqualMethodHandle.asSpreader(Book[].class, 1);
boolean ret = (boolean) isPriceEqualMethodHandle.invokeExact(book1, new Book[] {book2});
System.err.println(ret); // 输出:false
参数绑定 (Binding Arguments)
可以预绑定部分参数,生成一个新的MethodHandle,简化后续调用。Java 9中的字符串拼接优化就利用了此技术。
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodType mt = MethodType.methodType(String.class, String.class);
MethodHandle concatMethodHandle = lookup.findVirtual(String.class, "concat", mt);
// 预绑定前缀“pack_”
concatMethodHandle = concatMethodHandle.bindTo("pack_");
System.err.println(concatMethodHandle.invoke("xg")); // 输出:pack_xg
删除参数
当参数数量多于方法所需时,可以使用dropArguments方法动态地删除多余的参数。
public class MethodHanldeDropArgumentDemo {
public static String concat(String a) {
return "Pack_" + a;
}
public static void main(String[] args) throws Throwable {
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodType mt = MethodType.methodType(String.class, String.class);
MethodHandle concatMethodHandle = lookup.findStatic(MethodHanldeDropArgumentDemo.class, "concat", mt);
// 删除索引为1的String类型参数
concatMethodHandle = MethodHandles.dropArguments(concatMethodHandle, 1, String.class);
String ret = (String) concatMethodHandle.invokeExact("pack", "xg"); // “xg”参数被忽略
System.err.println(ret); // 输出:Pack_pack
}
}
性能基准测试
我们使用JMH(Java Microbenchmark Harness)进行性能基准测试,对比MethodHandle与反射在不同调用方式下的性能差异(单位:纳秒/操作,越低越好)。
| Benchmark (基准测试方法) |
Mode |
Cnt |
Score |
Error |
Units |
MethodHandleReflectTest.testMethodHandleInvoke |
avgt |
5 |
53.055 |
± 0.621 |
ns/op |
MethodHandleReflectTest.testMethodHandleInvokeExact |
avgt |
5 |
55.394 |
± 0.514 |
ns/op |
MethodHandleReflectTest.testMethodHandleInvokeWithArguments |
avgt |
5 |
156.647 |
± 2.313 |
ns/op |
MethodHandleReflectTest.testReflect |
avgt |
5 |
56.372 |
± 0.653 |
ns/op |
测试结果分析:
MethodHandle.invoke 和 MethodHandle.invokeExact 的性能与直接反射调用 (Method.invoke) 处于同一数量级,甚至略优。
MethodHandle.invokeWithArguments 由于支持可变参数和更宽松的类型匹配,性能开销显著增加。
- 在实际的Java高性能编程场景中,尤其是Spring Boot等框架底层,
MethodHandle(特别是invokeExact)因其接近直接调用的性能和无冗余安全检查的特性,成为替代反射进行动态调用的优先选择。然而,对于简单的、非高频的反射需求,标准的反射API因其易用性依然是合适的工具。