點擊上方“程序員蝸牛g”,選擇“設為星標”

跟蝸牛哥一起,每天進步一點點

性能調優!Java反射不如MethodHandle高性能調用_參數類型

程序員蝸牛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) ;

這裏調用後的返回值必須進行強制類型轉換(具體的類型),否則報如下錯誤:

性能調優!Java反射不如MethodHandle高性能調用_方法調用_02

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實戰語雀筆記,回覆面試、開發手冊、有超讚的粉絲福利