點擊上方“程序員蝸牛g”,選擇“設為星標”
跟蝸牛哥一起,每天進步一點點
程序員蝸牛g
大廠程序員一枚 跟蝸牛一起 每天進步一點點
33篇原創內容
公眾號
環境:SpringBoot3.4.2
1. 簡介
Java反射與MethodHandle均用於運行時動態操作方法,但設計目標與實現機制存在互補性。
- 反射通過Method對象封裝方法元信息,提供統一的調用接口,但每次調用需進行運行時權限檢查,導致性能損耗較大,且無法直接操作方法參數類型或順序。
- MethodHandle則通過預編譯的句柄(如MethodHandle.invokeExact)將安全檢查前置到創建階段,調用時直接跳轉至目標方法,性能接近直接調用,尤其適合高頻調用場景。
在動態調用方法的場景中,反射與MethodHandle性能差異顯著,本文將詳細介紹MethodHandle的使用以及對比二者性能差異。
2.實戰案例
創建並使用MethodHandle需要四個步驟:
- 創建查找對象(Lookup)
- 創建方法類型(MethodType)
- 查找方法句柄(MethodHandle)
- 調用方法句柄(invoke)
2.1 創建Lookup
創建方法句柄(MethodHandle)的第一步就是獲取查找對象(Lookup),這是一個工廠對象,負責為查找類可見的方法、構造函數和字段創建方法句柄。
我們可以通過 MethodHandles 創建不同訪問模式的Lookup。如下示例:
// 創建提供公共方法訪問權限的查找器
MethodHandles.Lookup publicLookup = MethodHandles.publicLookup() ;
// 如果需訪問私有方法和受保護方法,則可改用 lookup() 方法:
MethodHandles.Lookup lookup = MethodHandles.lookup() ;
2.2 創建方法類型MethodType
MethodType 表示方法句柄(MethodHandle)接受和返回的參數類型和返回類型。MethodType 的結構很簡單,它由一個返回類型和適當數量的參數類型組成,這些參數類型必須與方法句柄及其所有調用者正確匹配。與方法句柄一樣,MethodType的實例也是不可變的。如下示例:
創建一個返回類型是BigDecimal,接受一個參數類型是double的MethodType:
MethodType mt = MethodType.methodType(BigDecimal.class, double.class);
注意:如果方法返回原始類型或void作為其返回類型,我們將使用表示這些類型的類(void.class、int.class等)。
創建一個返回類型是void類型,接受一個參數類型是String的MethodType:
MethodType mt = MethodType.methodType(void.class, String.class);
2.3 查找方法句柄MethodHandle
有了MethodType對象,接下來,我們就可以通過Lookup查找具體的方法句柄了。
- 查找實例方法
通過findVirtual() 方法創建 MethodHandle。
如下示例,查找 String 類的 concat() 方法:
MethodHandles.Lookup lookup = MethodHandles.lookup() ;
MethodType mt = MethodType.methodType(String.class, String.class);
MethodHandle concatMethodHandle = lookup.findVirtual(String.class, "concat", mt);
- 查找靜態方法
使用 findStatic() 方法:
MethodType mt = MethodType.methodType(List.class, Object[].class) ;
MethodHandle asListMethodHandle = lookup.findStatic(Arrays.class, "asList", mt) ;
- 查找構造函數
使用findConstructor()方法查找構造函數:
這裏我們查找String類型的有參構造函數,參數類型是String.class
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) ;
注意:必須調用 Method#setAccessible 方法設置為true,否則執行MethodHandle調用時會報錯。
2.4 MethodHandle調用
得到了MethodHandle後,我們可以通過3種方式進行調用:
- 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方法調用
使用 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) ;
這裏調用後的返回值必須進行強制類型轉換(具體的類型),否則報如下錯誤:
2.5 數組參數展開
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) ;
isPriceEqualMethodHandle = isPriceEqualMethodHandle.asSpreader(Book[].class, 1) ;
boolean ret = (boolean) isPriceEqualMethodHandle.invokeExact(book1, new Book[] {book2}) ;
System.err.println(ret) ;
運行結果
false
2.6 增強MethodHandle
可以通過綁定參數(Binding Arguments)來增強MethodHandle的功能,而無需立即調用它。這種技術允許我們預填充部分參數,生成一個新的MethodHandle,從而簡化後續調用。Java 9的String拼接優化正是利用了這一機制。如下示例:
MethodHandles.Lookup lookup = MethodHandles.lookup() ;
MethodType mt = MethodType.methodType(String.class, String.class);
MethodHandle concatMethodHandle = lookup.findVirtual(String.class, "concat", mt);
concatMethodHandle = concatMethodHandle.bindTo("pack_");
System.err.println(concatMethodHandle.invoke("xg")) ;
運行結果
pack_xg
2.7 刪除參數
當你發現參數比實際方法需要的參數多的時候,你就需要動態的刪除參數了,如下示例:
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) ;
concatMethodHandle = MethodHandles.dropArguments(concatMethodHandle, 1, String.class) ;
String ret = (String) concatMethodHandle.invokeExact("pack", "xg") ;
System.err.println(ret) ;
}
}
該示例中,concat方法只接收一個參數,但是實際傳遞了2個參數,這時候我們通過MethodHandles#dropArguments方法參數多餘的參數。
運行結果
Pack_pack
2.8 性能測試
我們通過JMH測試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
如果這篇文章對您有所幫助,或者有所啓發的話,求一鍵三連:點贊、轉發、在看。
關注公眾號:woniuxgg,在公眾號中回覆:筆記 就可以獲得蝸牛為你精心準備的java實戰語雀筆記,回覆面試、開發手冊、有超讚的粉絲福利