方法句柄 方法句柄(method handle)是JSR 292中引入的一個重要概念,它是對Java中方法、構造方法和域的一個強類型的可執行的引用。這也是句柄這個詞的含義所在。通過方法句柄可以直接調用該句柄所引用的底層方法。從作用上來說,方法句柄的作用類似於2.2節中提到的反射API中的Method ...
目錄
方法句柄
方法句柄(method handle)是JSR 292中引入的一個重要概念,它是對Java中方法、構造方法和域的一個強類型的可執行的引用。這也是句柄這個詞的含義所在。通過方法句柄可以直接調用該句柄所引用的底層方法。從作用上來說,方法句柄的作用類似於2.2節中提到的反射API中的Method類,但是方法句柄的功能更強大、使用更靈活、性能也更好。實際上,方法句柄和反射API也是可以協同使用的,下麵會具體介紹。
在Java標準庫中,方法句柄是由java.lang.invoke.MethodHandle類來表示的。
1.方法句柄的類型
對於一個方法句柄來說,它的類型完全由它的參數類型和返回值類型來確定,而與它所引用的底層方法的名稱和所在的類沒有關係。比如引用String類的length方法和Integer類的intValue方法的方法句柄的類型就是一樣的,因為這兩個方法都沒有參數,而且返回值類型都是int。
在得到一個方法句柄,即MethodHandle類的對象之後,可以通過其type方法來查看其類型。該方法的返回值是一個java.lang.invoke.MethodType類的對象。MethodType類的所有對象實例都是不可變的,類似於String類。所有對MethodType類對象的修改,都會產生一個新的MethodType類對象。兩個MethodType類對象是否相等,只取決於它們所包含的參數類型和返回值類型是否完全一致。
1.1MethodType類的對象實例的創建
MethodType類的對象實例只能通過MethodType類中的靜態工廠方法來創建。這樣的工廠方法有三類。
1.1.1 通過指定參數和返回值的類型來創建MethodType.【顯式地指定返回值和參數的類型】
這主要是使用methodType方法的多種重載形式。使用這些方法的時候,至少需要指定返回值類型,而參數類型則可以是0到多個。
返回值類型總是出現在methodType方法參數列表的第一個,後面緊接著的是0到多個參數的類型。類型都是由Class類的對象來指定的。如果返回值類型是void,可以用void.class或java.lang.Void.class來聲明。
代碼清單2-31中給出了使用methodType方法的幾個示例。註意:最後一個methodType方法調用中使用了另外一個MethodType的參數類型作為當前MethodType類對象的參數類型。
代碼清單2-31 MethodType類中的methodType方法的使用示例
public void generateMethodTypes(){
//String.length()
MethodType mt1=MethodType.methodType(int.class);
//String.concat(String str)
MethodType mt2=MethodType.methodType(String.class, String.class);
//String.getChars(int srcBegin, int srcEnd, char[]dst, int dstBegin)
MethodType mt3=MethodType.methodType(void.class, int.class, int.class, char[].class, int.class);
//String.startsWith(String prefix)
MethodType mt4=MethodType.methodType(boolean.class, mt2);
}
1.1.2 通過靜態工廠方法genericMethodType來創建的
除了顯式地指定返回值和參數的類型之外,還可以生成通用的MethodType類型,即返回值和所有參數的類型都是Object類。
方法genericMethodType有兩種重載形式:
第一種形式只需要指明方法類型中包含的Object類型的參數個數即可。
第二種形式可以提供一個額外的參數來說明是否在參數列表的後面添加一個Object[]類型的參數。
在代碼清單2-32中,mt1有3個類型為Object的參數,而mt2有2個類型為Object的參數和後面的Object[]類型參數。
代碼清單2-32 生成通用MethodType類型的示例
public void generateGenericMethodTypes(){
MethodType mt1=MethodType.genericMethodType(3);
MethodType mt2=MethodType.genericMethodType(2,true);
}
1.1.2 通過靜態工廠方法fromMethodDescriptorString來創建的
最後介紹的一個工廠方法是比較複雜的fromMethodDescriptorString。這個方法允許開發人員指定方法類型在位元組代碼中的表示形式作為創建MethodType時的參數。這個方法的複雜之處在於位元組代碼中的方法類型格式不是很好理解。
比如代碼清單2-31中的String.getChars方法的類型在位元組代碼中的表示形式是“(II[CI)V”。不過這種格式比逐個聲明返回值和參數類型的做法更加簡潔,適合於對Java位元組代碼格式比較熟悉的開發人員。
在代碼清單2-33中,“(Ljava/lang/String;)Ljava/lang/String;”所表示的方法類型是返回值和參數類型都是java.lang.String,相當於使用MethodType.methodType(String.class, String.class)。
代碼清單2-33 使用方法類型在位元組代碼中的表示形式來創建MethodType
public void generateMethodTypesFromDescriptor(){
ClassLoader cl=this.getClass().getClassLoader();
String descriptor="(Ljava/lang/String;)Ljava/lang/String;";
MethodType mt1=MethodType.fromMethodDescriptorString(descriptor, cl);
}
註意:在使用fromMethodDescriptorString方法的時候,需要指定一個類載入器。該類載入器用來載入方法類型表達式中出現的Java類。如果不指定,預設使用系統類載入器。
2 對MethodType類的對象實例的修改
2.1 圍繞返回值和參數類型的精確修改
在通過工廠方法創建出MethodType類的對象實例之後,可以對其進行進一步修改。這些修改都圍繞返回值和參數類型展開。所有這些修改方法都返回另外一個新的MethodType對象。
代碼清單2-34 對MethodType中的返回值和參數類型進行修改的示例
public void changeMethodType(){
//(int, int)String
MethodType mt=MethodType.methodType(String.class, int.class, int.class);
//(int, int, float)String
mt=mt.appendParameterTypes(float.class);
//(int, double, long, int, float)String
mt=mt.insertParameterTypes(1,double.class, long.class);
//(int, double, int, float)String
mt=mt.dropParameterTypes(2,3);
//(int, double, String, float)String
mt=mt.changeParameterType(2,String.class);
//(int, double, String, float)void
mt=mt.changeReturnType(void.class);
}
2.2 一次性對返回值和所有參數的類型進行修改
除了上面這幾個精確修改返回值和參數的類型的方法之外,MethodType還有幾個可以一次性對返回值和所有參數的類型進行處理的方法。
代碼清單2-35給出了這幾個方法的使用示例,其中wrap和unwrap用來在基本類型及其包裝類型之間進行轉換,generic方法把所有返回值和參數類型都變成Object類型,而erase只把引用類型變成Object,並不處理基本類型。修改之後的方法類型同樣以註釋的形式給出。
代碼清單2-35 一次性修改MethodType中的返回值和所有參數的類型的示例
public void wrapAndGeneric(){
//(int, double)Integer
MethodType mt=MethodType.methodType(Integer.class, int.class, double.class);
//(Integer, Double)Integer
MethodType wrapped=mt.wrap();
//(int, double)int
MethodType unwrapped=mt.unwrap();
//(Object, Object)Object
MethodType generic=mt.generic();
//(int, double)Object
MethodType erased=mt.erase();
}
由於每個對MethodType對象進行修改的方法的返回值都是一個新的MethodType對象,可以很容易地通過方法級聯來簡化代碼。
3.方法句柄的調用
在獲取到了一個方法句柄之後,最直接的使用方法就是調用它所引用的底層方法。在這點上,方法句柄的使用類似於反射API中的Method類。但是方法句柄在調用時所提供的靈活性是Method類中的invoke方法所不能比的。
3.1 通過invokeExact方法實現
最直接的調用一個方法句柄的做法是通過invokeExact方法實現的。這個方法與直接調用底層方法是完全一樣的。
invokeExact方法的參數依次是作為方法接收者的對象和調用時候的實際參數列表。
比如在代碼清單2-36中,這種調用方式就相當於直接調用"Hello World".substring(1,3)
代碼清單2-36 使用invokeExact方法調用方法句柄
public void invokeExact()throws Throwable{
// 1.先獲取String類中substring的方法句柄.
MethodHandles.Lookup lookup=MethodHandles.lookup();
MethodType type=MethodType.methodType(String.class, int.class, int.class);
MethodHandle mh=lookup.findVirtual(String.class,"substring",type);
// 2.再通過invokeExact來進行調用。
String str=(String)mh.invokeExact("Hello World",1,3);
System.out.println(str);
}
在這裡強調一下靜態方法和一般方法之間的區別。靜態方法在調用時是不需要指定方法的接收對象的,而一般的方法則是需要的。如果方法句柄mh所引用的是java.lang.Math類中的靜態方法min,那麼直接通過mh.invokeExact(3,4)就可以調用該方法。
註意:invokeExact方法在調用的時候要求嚴格的類型匹配,方法的返回值類型也是在考慮範圍之內的。代碼清單2-36中的方法句柄所引用的substring方法的返回值類型是String,因此在使用invokeExact方法進行調用時,需要在前面加上強制類型轉換,以聲明返回值的類型。
如果去掉這個類型轉換,而直接賦值給一個Object類型的變數,在調用的時候會拋出異常,因為invokeExact會認為方法的返回值類型是Object。如下圖所示:
去掉類型轉換但是不進行賦值操作也是錯誤的,因為invokeExact會認為方法的返回值類型是void,也不同於方法句柄要求的String類型的返回值。
3.1 通過invoke方法實現
與invokeExact所要求的類型精確匹配不同的是,invoke方法允許更加鬆散的調用方式。它會嘗試在調用的時候進行返回值和參數類型的轉換工作。這是通過MethodHandle類的asType方法來完成的。asType方法的作用是把當前的方法句柄適配到新的MethodType上,並產生一個新的方法句柄。當方法句柄在調用時的類型與其聲明的類型完全一致的時候,調用invoke等同於調用invokeExact;否則,invoke會先調用asType方法來嘗試適配到調用時的類型。如果適配成功,調用可以繼續;否則會拋出相關的異常。這種靈活的適配機制,使invoke方法成為在絕大多數情況下都應該使用的方法句柄調用方式。
進行類型適配的基本規則是比對返回值類型和每個參數的類型是否都可以相互匹配。只要返回值類型或某個參數的類型無法完成匹配,那麼整個適配過程就是失敗的。從待轉換的源類型S到目標類型T匹配成功的基本原則如下:
- 1)可以通過Java的類型轉換來完成,一般是從子類轉換成父類,介面的實現類轉換成介面,比如從String類轉換到Object類
- 2)可以通過基本類型的轉換來完成,只能進行類型範圍的擴大,比如從int類型轉換到long類型。
- 3)可以通過基本類型的自動裝箱和拆箱機制來完成,比如從int類型到Integer類型。
- 4)如果S有返回值類型,而T的返回值是void, S的返回值會被丟棄。
- 5)如果S的返回值是void,而T的返回值是引用類型,T的返回值會是null。
- 6)如果S的返回值是void,而T的返回值是基本類型,T的返回值會是0。
滿足上面規則時進行兩個方法類型之間的轉換是會成功的。
let's see how it's possible to use the invoke() with a boxed argument:
@Test
public void givenReplaceMethodHandle_whenInvoked_thenCorrectlyReplaced() throws Throwable {
MethodHandles.Lookup publicLookup = MethodHandles.publicLookup();
MethodType mt = MethodType.methodType(String.class, char.class, char.class);
MethodHandle replaceMH = publicLookup.findVirtual(String.class, "replace", mt);
String replacedString = (String) replaceMH.invoke("jovo", Character.valueOf('o'), 'a');
String replacedString3 = (String) replaceMH.invoke("jovo", 'o', 'a');
String replacedString2 = (String) replaceMH.invoke("jovo", new Character('o'), 'a');
String replacedString4 = (String) replaceMH.invokeExact("jovo", 'o', 'a');
String replacedString5 = (String) replaceMH.invokeExact("jovo", new Character('o'), 'a'); //不能使用包裝類,報錯
assertEquals("java", replacedString);
}
In this case, the replaceMH requires char arguments, the invoke() performs an unboxing on the Character argument before its execution.通過MethodHandle類的asType方法嘗試在調用的時候進行參數類型的轉換工作。
3.3 通過invokeWithArguments方法實現
最後一種調用方式是使用invokeWithArguments。該方法在調用時可以指定任意多個Object類型的參數。完整的調用方式是首先根據傳入的實際參數的個數.
-
- 通過MethodType的genericMethodType方法得到一個返回值和參數類型都是Object的新方法類型。
-
- 再把原始的方法句柄通過asType轉換後得到一個新的方法句柄。
-
- 最後通過新方法句柄的invokeExact方法來完成調用。
這個方法相對於invokeExact和invoke的優勢在於,它可以通過Java反射API被正常獲取和調用,而invokeExact和invoke不可以這樣。它可以作為反射API和方法句柄之間的橋梁。
MethodType mt = MethodType.methodType(List.class, Object[].class);
MethodHandle asList = publicLookup.findStatic(Arrays.class, "asList", mt);
List<Integer> list = (List<Integer>) asList.invokeWithArguments(1,2);
assertThat(Arrays.asList(1,2), is(list));
methodHandle類中的invokeWithArguments方法
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);
}
4.參數長度可變的方法句柄 --- 簡化方法調用時的語法
在方法句柄中,所引用的底層方法中包含長度可變的參數是一種比較特殊的情況。雖然最後一個長度可變的參數實際上是一個數組,但是仍然可以簡化方法調用時的語法。對於這種特殊的情況,方法句柄也提供了相關的處理能力,主要是一些轉換的方法,允許在可變長度的參數和數組類型的參數之間互相轉換,以方便開發人員根據需求選擇最適合的調用語法.
4.1 MethodHandle的asVarargsCollector方法
MethodHandle中第一個與長度可變參數相關的方法是asVarargsCollector。它的作用是把原始的方法句柄中的最後一個數組類型的參數轉換成對應類型的可變長度參數。
如代碼清單2-37所示,方法normalMethod的最後一個參數是int類型的數組,引用它的方法句柄在通過asVarargsCollector方法轉換之後,得到的新方法句柄在調用時就可以使用長度可變參數的語法格式,而不需要使用原始的數組形式。在實際的調用中,int類型的參數3、4和5組成的數組被傳入到了normalMethod的參數arg3中。
代碼清單2-37 asVarargsCollector方法的使用示例
public class Varargs {
public void normalMethod(String arg1,int arg2,int[]arg3){
System.out.println(arg3); // args
}
@Test
public void asVarargsCollector()throws Throwable{
MethodHandles.Lookup lookup=MethodHandles.lookup();
MethodHandle mh=lookup.findVirtual(Varargs.class,"normalMethod", MethodType.methodType(void.class, String.class,
int.class, int[].class));
mh = mh.asVarargsCollector(int[].class);
mh.invoke(this,"Hello",2,1,4,5,7,8);
}
}
4.2 MethodHandle的asCollector方法
第二個方法asCollector的作用與asVarargsCollector類似,不同的是該方法只會把指定數量的參數;收集到原始方法句柄所對應的底層方法的數組類型參數中,而不像asVarargsCollector那樣可以收集任意數量的參數。
如代碼清單2-38所示,還是以引用normalMethod的方法句柄為例,asCollector方法調用時的指定參數為2,即只有2個參數會被收集到整數類型數組中。在實際的調用中,int類型的參數3和4組成的數組被傳入到了normalMethod的參數args中。
代碼清單2-38 asCollector方法的使用示例
public class Varargs {
public void normalMethod(String arg1,int arg2,int[]arg3){
System.out.println(arg3);
}
@Test
public void asCollector()throws Throwable{
MethodHandles.Lookup lookup=MethodHandles.lookup();
MethodHandle mh=lookup.findVirtual(Varargs.class,"normalMethod", MethodType.methodType(void.class, String.class,
int.class, int[].class));
mh = mh.asCollector(int[].class,2);
mh.invoke(this,"Hello",2,1,4);
// mh.invoke(this,"Hello",2,1,4,5,7,8); // 報錯了指定最後一個入參數組的長度為2
}
}
4.3MethodHandle的asSpreader方法
上面的兩個方法把數組類型參數轉換為長度可變的參數,自然還有與之對應的執行反方向轉換的方法。
代碼清單2-39給出的asSpreader方法就把長度可變的參數轉換成數組類型的參數。轉換之後的新方法句柄在調用時使用數組作為參數,而數組中的元素會被按順序分配給原始方法句柄中的各個參數。在實際的調用中,toBeSpreaded方法所接受到的參數arg2、arg3和arg4的值分別是3、4和5。
代碼清單2-39 asSpreader方法的使用示例
public void toBeSpreaded (String arg1,int arg2,int arg3,int arg4){
}
public void asSpreader()throws Throwable {
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle mh = lookup.findVirtual(Varargs.class, "toBeSpreaded", MethodType.methodType(void.class, String.class,
int.class, int.class, int.class));
mh = mh.asSpreader(int[].class, 3);
mh.invoke(this, "Hello", new int[]{3, 4, 5});
}
}
4.3MethodHandle的asFixedArity方法
最後一個方法asFixedArity是把參數長度可變的方法轉換成參數長度不變的方法。經過這樣的轉換之後,最後一個長度可變的參數實際上就變成了對應的數組類型。在調用方法句柄的時候,就只能使用數組來進行參數傳遞。
如代碼清單2-40所示,asFixedArity會把引用參數長度可變方法varargsMethod的原始方法句柄轉換成固定長度參數的方法句柄。
代碼清單2-40 asFixedArity方法的使用示例
public void varargsMethod(String arg1,int...args){
}
public void asFixedArity()throws Throwable{
MethodHandles.Lookup lookup=MethodHandles.lookup();
MethodHandle mh=lookup.findVirtual(Varargs.class,"varargsMethod",MethodType.methodType(void.class, String.class,
int[].class));
mh=mh.asFixedArity();
mh.invoke(this,"Hello",new int[]{2,4});
}
5.參數綁定
在前面介紹過,如果方法句柄在調用時引用的底層方法不是靜態的,調用的第一個參數應該是該方法調用的接收者。這個參數的值一般在調用時指定,也可以事先進行綁定。通過MethodHandle的bindTo方法可以預先綁定底層方法的調用接收者,在實際調用的時候,只需要傳入實際參數即可,不需要再指定方法的接收者。
代碼清單2-41給出了對引用String類的length方法的方法句柄的兩種調用方式:
- 第一種沒有進行綁定,調用時需要傳入length方法的接收者;
- 第二種方法預先綁定了一個String類的對象,因此調用時不需要再指定。
代碼清單2-41 參數綁定的基本用法
public void bindTo()throws Throwable{
MethodHandles.Lookup lookup=MethodHandles.lookup();
MethodHandle mh=lookup.findVirtual(String.class,"length",MethodType.methodType(int.class));
int len=(int)mh.invoke("Hello");//值為5
mh=mh.bindTo("Hello World");
len=(int)mh.invoke();//值為11
}
優點:這種預先綁定參數的方式的靈活性在於它允許開發人員只公開某個方法,而不公開該方法所在的對象。開發人員只需要找到對應的方法句柄,並把適合的對象綁定到方法句柄上,客戶代碼就可以只獲取到方法本身,而不會知道包含此方法的對象。綁定之後的方法句柄本身就可以在任何地方直接運行。
實際上,MethodHandle的bindTo方法只是綁定方法句柄的第一個參數而已,並不要求這個參數一定表示方法調用的接收者。對於一個MethodHandle,可以多次使用bindTo方法來為其中的多個參數綁定值。代碼清單2-42給出了多次綁定的一個示例。方法句柄所引用的底層方法是String類中的indexOf方法,同時為方法句柄的前兩個參數分別綁定了具體的值。
代碼清單2-42 多次參數綁定的示例
@Test
public void multipleBindTo()throws Throwable{
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle mh = lookup.findVirtual(String.class,"indexOf",MethodType.methodType(
int.class, String.class, int.class));
mh = mh.bindTo("Hello").bindTo("l");
int index = "Hello".indexOf('l',2);
assertEquals(index, mh.invoke(2)); // true
}
需要註意的是,在進行參數綁定的時候,只能對引用類型的參數進行綁定。無法為int和float這樣的基本類型綁定值。對於包含基本類型參數的方法句柄,可以先使用wrap方法把方法類型中的基本類型轉換成對應的包裝類,再通過方法句柄的asType將其轉換成新的句柄。轉換之後的新句柄就可以通過bindTo來進行綁定,如代碼清單2-43所示。
代碼清單2-43 基本類型參數的綁定方式
@Test
public void multipleBindTo()throws Throwable{
MethodHandles.Lookup lookup = MethodHandles.lookup();
// MethodHandle mh = lookup.findVirtual(String.class,"indexOf",MethodType.methodType(
// int.class, String.class, int.class));
// mh = mh.bindTo("Hello").bindTo("l");
// int index = "Hello".indexOf('l',2);
// assertEquals(index, mh.invoke(2));
MethodHandle mh=lookup.findVirtual(String.class,"substring",MethodType.methodType(String.class, int.class,
int.class));
mh=mh.asType(mh.type().wrap());
mh=mh.bindTo("Hello World").bindTo(3);
String str = "Hello World".substring(3,5);
System.out.println(mh.invoke(5));//值為“lo”
assertEquals(str, mh.invoke(5));
}
參考:
https://www.baeldung.com/java-method-handles