目錄
- 方法句柄
- 1.方法句柄的型別
- 1.1MethodType類的物件實體的創建
- 1.1.1 通過指定引數和回傳值的型別來創建MethodType.【顯式地指定回傳值和引數的型別】
- 1.1.2 通過靜態工廠方法genericMethodType來創建的
- 1.1.2 通過靜態工廠方法fromMethodDescriptorString來創建的
- 1.1MethodType類的物件實體的創建
- 2 對MethodType類的物件實體的修改
- 2.1 圍繞回傳值和引數型別的精確修改
- 2.2 一次性對回傳值和所有引數的型別進行修改
- 3.方法句柄的呼叫
- 3.1 通過invokeExact方法實作
- 3.1 通過invoke方法實作
- 3.3 通過invokeWithArguments方法實作
- 4.引數長度可變的方法句柄 --- 簡化方法呼叫時的語法
- 4.1 MethodHandle的asVarargsCollector方法
- 4.2 MethodHandle的asCollector方法
- 4.3MethodHandle的asSpreader方法
- 4.3MethodHandle的asFixedArity方法
- 5.引數系結
- 1.方法句柄的型別
方法句柄
??方法句柄(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
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/178807.html
標籤:Java
