JDK7的動態類型,關於java.lang.invoke包的解釋

來源:深入理解Java 7:核心技術與最佳實踐

方法句柄(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類對象是否相等,只取決於它們所包含的參數類型和返回值類型是否完全一致。

MethodType類的對象實例只能通過MethodType類中的靜態工廠方法來創建。這樣的工廠方法有三類。第一類是通過指定參數和返回值的類型來創建MethodType,這主要是使用methodType方法的多種重載形式。使用這些方法的時候,至少需要指定返回值類型,而參數類型則可以是0到多個。返回值類型總是出現在methodType方法參數列表的第一個,後面緊接着的是0到多個參數的類型。類型都是由Class類的對象來指定的。如果返回值類型是void,可以用void.class或java.lang.Void.class來聲明。代碼清單2-31中給出了使用methodType方法的幾個示例。每個MethodType聲明上以註釋的方式給出了與之相匹配的String類中的一個方法。這裏值得一提的是,最後一個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);
}

除了顯式地指定返回值和參數的類型之外,還可以生成通用的MethodType類型,即返回值和所有參數的類型都是Object類。這是通過靜態工廠方法genericMethodType來創建的。方法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);
}

最後介紹的一個工廠方法是比較複雜的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類。如果不指定,默認使用系統類加載器。

在通過工廠方法創建出MethodType類的對象實例之後,可以對其進行進一步修改。這些修改都圍繞返回值和參數類型展開。所有這些修改方法都返回另外一個新的MethodType對象。代碼清單2-34給出了對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);
}

除了上面這幾個精確修改返回值和參數的類型的方法之外,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對象,可以很容易地通過方法級聯來簡化代碼。

2.方法句柄的調用

在獲取到了一個方法句柄之後,最直接的使用方法就是調用它所引用的底層方法。在這點上,方法句柄的使用類似於反射API中的Method類。但是方法句柄在調用時所提供的靈活性是Method類中的invoke方法所不能比的。

最直接的調用一個方法句柄的做法是通過invokeExact方法實現的。這個方法與直接調用底層方法是完全一樣的。invokeExact方法的參數依次是作爲方法接收者的對象和調用時候的實際參數列表。比如在代碼清單2-36中,先獲取String類中substring的方法句柄,再通過invokeExact來進行調用。這種調用方式就相當於直接調用"Hello World".substring(1, 3)。關於方法句柄的獲取,下一節會具體介紹。

代碼清單2-36 使用invokeExact方法調用方法句柄
public void invokeExact() throws Throwable {
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodType type = MethodType.methodType(String.class, int.class, int.class);
MethodHandle mh = lookup.findVirtual(String.class, "substring", type);
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類型的返回值。

與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。

滿足上面規則時進行兩個方法類型之間的轉換是會成功的。對於invoke方法的具體使用,只需要把代碼清單2-36中的invokeExact方法換成invoke即可,並不需要做太多的介紹。

最後一種調用方式是使用invokeWithArguments。該方法在調用時可以指定任意多個Object類型的參數。完整的調用方式是首先根據傳入的實際參數的個數,通過MethodType的genericMethodType方法得到一個返回值和參數類型都是Object的新方法類型。再把原始的方法句柄通過asType轉換後得到一個新的方法句柄。最後通過新方法句柄的invokeExact方法來完成調用。這個方法相對於invokeExact和invoke的優勢在於,它可以通過Java反射API被正常獲取和調用,而invokeExact和invoke不可以這樣。它可以作爲反射API和方法句柄之間的橋樑。

3.參數長度可變的方法句柄

在方法句柄中,所引用的底層方法中包含長度可變的參數是一種比較特殊的情況。雖然最後一個長度可變的參數實際上是一個數組,但是仍然可以簡化方法調用時的語法。對於這種特殊的情況,方法句柄也提供了相關的處理能力,主要是一些轉換的方法,允許在可變長度的參數和數組類型的參數之間互相轉換,以方便開發人員根據需求選擇最適合的調用語法。

MethodHandle中第一個與長度可變參數相關的方法是asVarargsCollector。它的作用是把原始的方法句柄中的最後一個數組類型的參數轉換成對應類型的可變長度參數。如代碼清單2-37所示,方法normalMethod的最後一個參數是int類型的數組,引用它的方法句柄在通過asVarargsCollector方法轉換之後,得到的新方法句柄在調用時就可以使用長度可變參數的語法格式,而不需要使用原始的數組形式。在實際的調用中,int類型的參數3、4和5組成的數組被傳入到了normalMethod的參數args中。

代碼清單2-37 asVarargsCollector方法的使用示例
public void normalMethod(String arg1, int arg2, int[] arg3) {
}

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, 3, 4, 5);
}

第二個方法asCollector的作用與asVarargsCollector類似,不同的是該方法只會把指定數量的參數收集到原始方法句柄所對應的底層方法的數組類型參數中,而不像asVarargsCollector那樣可以收集任意數量的參數。如代碼清單2-38所示,還是以引用normalMethod的方法句柄爲例,asCollector方法調用時的指定參數爲2,即只有2個參數會被收集到整數類型數組中。在實際的調用中,int類型的參數3和4組成的數組被傳入到了normalMethod的參數args中。

代碼清單2-38 asCollector方法的使用示例
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, 3, 4);
}

上面的兩個方法把數組類型參數轉換爲長度可變的參數,自然還有與之對應的執行反方向轉換的方法。代碼清單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});
}

最後一個方法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});
}

4.參數綁定

在前面介紹過,如果方法句柄在調用時引用的底層方法不是靜態的,調用的第一個參數應該是該方法調用的接收者。這個參數的值一般在調用時指定,也可以事先進行綁定。通過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 多次參數綁定的示例
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");
System.out.println(mh.invoke(2)); //值爲2
}

需要注意的是,在進行參數綁定的時候,只能對引用類型的參數進行綁定。無法爲int和float這樣的基本類型綁定值。對於包含基本類型參數的方法句柄,可以先使用wrap方法把方法類型中的基本類型轉換成對應的包裝類,再通過方法句柄的asType將其轉換成新的句柄。轉換之後的新句柄就可以通過bindTo來進行綁定,如代碼清單2-43所示。

代碼清單2-43 基本類型參數的綁定方式
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);
System.out.println(mh.invoke(5)); //值爲“lo”

5.獲取方法句柄

獲取方法句柄最直接的做法是從一個類中已有的方法中轉換而來,得到的方法句柄直接引用這個底層方法。在之前的示例中都是通過這種方式來獲取方法句柄的。方法句柄可以按照與反射API類似的做法,從已有的類中根據一定的條件進行查找。與反射API不同的是,方法句柄並不區分構造方法、方法和域,而是統一轉換成MethodHandle對象。對於域來說,獲取到的是用來獲取和設置該域的值的方法句柄。

方法句柄的查找是通過java.lang.invoke.MethodHandles.Lookup類來完成的。在查找之前,需要通過調用MethodHandles.lookup方法獲取到一個MethodHandles.Lookup類的對象。MethodHandles.Lookup類提供了一些方法以根據不同的條件進行查找。代碼清單2-44以String類爲例說明了查找構造方法和一般方法的示例。方法findConstructor用來查找類中的構造方法,需要指定返回值和參數類型,即MethodType對象。而findVirtual和findStatic則用來查找一般方法和靜態方法,除了表示方法的返回值和參數類型的MethodType對象之外,還需要指定方法的名稱。

代碼清單2-44 查找構造方法、一般方法和靜態方法的方法句柄的示例
public void lookupMethod() throws NoSuchMethodException, IllegalAccessException {
MethodHandles.Lookup lookup = MethodHandles.lookup();
//構造方法
lookup.findConstructor(String.class, MethodType.methodType(void.class, byte[].class));
//String.substring
lookup.findVirtual(String.class, "substring", MethodType.methodType(String.class, int.class, int.class));
//String.format
lookup.findStatic(String.class, "format", MethodType.methodType(String.class, String.class, Object[].class));
}

除了上面3種類型的方法之外,還有一個findSpecial方法用來查找類中的特殊方法,主要是類中的私有方法。代碼清單2-45給出了findSpecial的使用示例,Method-HandleLookup是lookupSpecial方法所在的類,而privateMethod是該類中的一個私有方法。由於訪問的是類的私有方法,從訪問控制的角度出發,進行方法查找的類需要具備訪問私有方法的權限。

代碼清單2-45 查找類中特殊方法的方法句柄的示例
public MethodHandle lookupSpecial() throws NoSuchMethodException, IllegalAccessException, Throwable {
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle mh = lookup.findSpecial(MethodHandleLookup.class, "privateMethod", MethodType.methodType(void.class), MethodHandleLookup.class);
return mh;
}

從上面的代碼中可以看到,findSpecial方法比之前的findVirtual和findStatic等方法多了一個參數。這個額外的參數用來指定私有方法被調用時所使用的類。提供這個類的原因是爲了滿足對私有方法的訪問控制的要求。當方法句柄被調用時,指定的調用類必須具備訪問私有方法的權限,否則會出現無法訪問的錯誤。

除了類中本來就存在的方法之外,對域的處理也是通過相應的獲取和設置域的值的方法句柄來完成的。代碼清單2-46說明了如何查找到類中的靜態域和一般域所對應的獲取和設置的方法句柄。在查找的時候只需要提供域所在的類的Class對象、域的名稱和類型即可。

代碼清單2-46  查找類中的靜態域和一般域對應的獲取和設置的方法句柄的示例
public void lookupFieldAccessor() throws NoSuchFieldException, Illegal-AccessException{
MethodHandles.Lookup lookup = MethodHandles.lookup();
lookup.findGetter(Sample.class, "name", String.class);
lookup.findSetter(Sample.class, "name", String.class);
lookup.findStaticGetter(Sample.class, "value", int.class);
lookup.findStaticSetter(Sample.class, "value", int.class);
}

對於靜態域來說,調用其對應的獲取和設置值的方法句柄時,並不需要提供調用的接收者對象作爲參數。而對於一般域來說,該對象在調用時是必需的。

除了直接在某個類中進行查找之外,還可以從通過反射API得到的Constructor、Field和Method等對象中獲得方法句柄。如代碼清單2-47所示,首先通過反射API得到表示構造方法的Constructor對象,再通過unreflectConstructor方法就可以得到其對應的一個方法句柄;而通過unreflect方法可以將Method類對象轉換成方法句柄。對於私有方法,則需要使用unreflectSpecial來進行轉換,同樣也需要提供一個作用與findSpecial中參數相同的額外參數;對於Field類的對象來說,通過unreflectGetter和unreflectSetter就可以得到獲取和設置其值的方法句柄。

代碼清單2-47 通過反射API獲取方法句柄的示例
public void unreflect() throws Throwable {
MethodHandles.Lookup lookup = MethodHandles.lookup();
Constructor constructor = String.class.getConstructor(byte[].class);
lookup.unreflectConstructor(constructor);
Method method = String.class.getMethod("substring", int.class, int.class);
lookup.unreflect(method);

Method privateMethod = ReflectMethodHandle.class.getDeclaredMethod("privateMethod");
lookup.unreflectSpecial(privateMethod, ReflectMethodHandle.class);

Field field = ReflectMethodHandle.class.getField("name");
lookup.unreflectGetter(field);
lookup.unreflectSetter(field);
}

除了通過在Java類中進行查找來獲取方法句柄外,還可以通過java.lang.invoke.MethodHandles中提供的一些靜態工廠方法來創建一些通用的方法句柄。

第一個方法是用來對數組進行操作的,即得到可以用來獲取和設置數組中元素的值的方法句柄。這些工廠方法的作用等價於2.2.4節介紹的反射API中的java.lang.reflect.Array類中的靜態方法。如代碼清單2-48所示,MethodHandles的arrayElementGetter和arrayElementSetter方法分別用來得到獲取和設置數組元素的值的方法句柄。調用這些方法句柄就可以對數組進行操作。

代碼清單2-48 獲取和設置數組中元素的值的方法句柄的使用示例
public void arrayHandles() throws Throwable {
int[] array = new int[] {1, 2, 3, 4, 5};
MethodHandle setter = MethodHandles.arrayElementSetter(int[].class);
setter.invoke(array, 3, 6);
MethodHandle getter = MethodHandles.arrayElementGetter(int[].class);
int value = (int) getter.invoke(array, 3); //值爲6
}

MethodHandles中的靜態方法identity的作用是通過它所生成的方法句柄,在每次調用的時候,總是返回其輸入參數的值。如代碼清單2-49所示,在使用identity方法的時候只需要傳入方法句柄的唯一參數的類型即可,該方法句柄的返回值類型和參數類型是相同的。

代碼清單2-49 MethodHandles類的identity方法的使用示例
public void identity() throws Throwable {
MethodHandle mh = MethodHandles.identity(String.class);
String value = (String) mh.invoke("Hello"); //值爲"Hello"
}

而方法constant的作用則更加簡單,在生成的時候指定一個常量值,以後這個方法句柄被調用的時候,總是返回這個常量值,在調用時也不需要提供任何參數。這個方法提供了一種把一個常量值轉換成方法句柄的方式,如下面的代碼所示。在調用constant方法的時候,只需要提供常量的類型和值即可。

代碼清單2-50 MethodHandles類的constant方法的使用示例
public void constant() throws Throwable {
MethodHandle mh = MethodHandles.constant(String.class, "Hello");
String value = (String) mh.invoke(); //值爲"Hello"
}

MethodHandles類中的identity方法和constant方法的作用類似於在開發中用到的“空對象(Null object)”模式的應用。在使用方法句柄的某些場合中,如果沒有合適的方法句柄對象,可能不允許直接用null來替換,這個時候可以通過這兩個方法來生成簡單無害的方法句柄對象作爲替代。

6.方法句柄變換

方法句柄的強大之處在於可以對它進行各種不同的變換操作。這些變換操作包括對方法句柄的返回值和參數的處理等,同時這些單個的變換操作可以組合起來,形成複雜的變換過程。所有的這些變換方法都是MethodHandles類中的靜態方法。這些方法一般接受一個已有的方法句柄對象作爲變換的來源,而方法的返回值則是變換操作之後得到的新的方法句柄。下面的內容中經常出現的“原始方法句柄”表示的是變換之前的方法句柄,而“新方法句柄”則表示變換之後的方法句柄。

首先介紹對參數進行處理的變換方法。在調用變換之後的新方法句柄時,調用時的參數值會經過一定的變換操作之後,再傳遞給原始的方法句柄來完成具體的執行。

第一個方法dropArguments可以在一個方法句柄的參數中添加一些無用的參數。這些參數雖然在實際調用時不會被使用,但是它們可以使變換之後的方法句柄的參數類型格式符合某些所需的特定模式。這也是這種變換方式的主要應用場景。

如代碼清單2-51所示,原始的方法句柄mhOld引用的是String類中的substring方法,其類型是String類的返回值加上兩個int類型的參數。在調用dropArguments方法的時候,第一個參數表示待變換的方法句柄,第二個參數指定的是要添加的新參數類型在原始參數列表中的起始位置,其後的多個參數類型將被添加到參數列表中。新的方法句柄mhNew的參數類型變爲float、String、String、int和int,而在實際調用時,前面兩個參數的值會被忽略掉。可以把這些多餘的參數理解成特殊調用模式所需要的佔位符。

代碼清單2-51 dropArguments方法的使用示例
public void dropArguments() throws Throwable {
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodType type = MethodType.methodType(String.class, int.class, int.class);
MethodHandle mhOld = lookup.findVirtual(String.class, "substring", type);
String value = (String) mhOld.invoke("Hello", 2, 3);
MethodHandle mhNew = MethodHandles.dropArguments(mhOld, 0, float.class, String.class);
value = (String) mhNew.invoke(0.5f, "Ignore", "Hello", 2, 3);
}

第二個方法insertArguments的作用與本小節前面提到的MethodHandle的bindTo方法類似,但是此方法的功能更加強大。這個方法可以同時爲方法句柄中的多個參數預先綁定具體的值。在得到的新方法句柄中,已經綁定了具體值的參數不再需要提供,也不會出現在參數列表中。

在代碼清單2-52中,方法句柄mhOld所表示的底層方法是String類中的concat方法。在調用insertArguments方法的時候,與上面的dropArguments方法類似,從第二個參數所指定的參數列表中的位置開始,用其後的可變長度的參數的值作爲預設值,分別綁定到對應的參數上。在這裏把mhOld的第二個參數的值預設成了固定值“--”,其作用是在調用新方法句柄時,只需要傳入一個參數即可,相當於總是與“--”進行字符串連接操作,即使用“--”作爲後綴。由於有一個參數被預先設置了值,因此mhNew在調用時只需要一個參數即可。如果預先綁定的是方法句柄mhOld的第一個參數,那就相當於用字符串“--”來連接各種不同的字符串,即爲字符串添加“--”作爲前綴。如果insertArguments方法調用時指定了多個綁定值,會按照第二個參數指定的起始位置,依次進行綁定。

代碼清單2-52 insertArguments方法的使用示例
public void insertArguments() throws Throwable {
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodType type = MethodType.methodType(String.class, String.class);
MethodHandle mhOld = lookup.findVirtual(String.class, "concat", type);
String value = (String) mhOld.invoke("Hello", "World");
MethodHandle mhNew = MethodHandles.insertArguments(mhOld, 1, " --");
value = (String) mhNew.invoke("Hello"); //值爲“Hello--”
}

第三個方法filterArguments的作用是可以對方法句柄調用時的參數進行預處理,再把預處理的結果作爲實際調用時的參數。預處理的過程是通過其他的方法句柄來完成的。可以對一個或多個參數指定用來進行處理的方法句柄。代碼清單2-53給出了filterArguments方法的使用示例。要執行的原始方法句柄所引用的是Math類中的max方法,而在實際調用時傳入的卻是兩個字符串類型的參數。中間的參數預處理是通過方法句柄mhGetLength來完成的,該方法句柄的作用是獲得字符串的長度。這樣就可以把字符串類型的參數轉換成原始方法句柄所需要的整數類型。完成預處理之後,將處理的結果交給原始方法句柄來完成調用。

代碼清單2-53 filterArguments方法的使用示例
public void filterArguments() throws Throwable {
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodType type = MethodType.methodType(int.class, int.class, int.class);
MethodHandle mhGetLength = lookup.findVirtual(String.class, "length", MethodType.methodType(int.class));
MethodHandle mhTarget = lookup.findStatic(Math.class, "max", type);
MethodHandle mhNew = MethodHandles.filterArguments(mhTarget, 0, mhGetLength, mhGetLength);
int value = (int) mhNew.invoke("Hello", "New World"); //值爲9
}

在使用filterArguments的時候,第二個參數和後面的可變長度的方法句柄參數是配合起來使用的。第二個參數指定的是進行預處理的方法句柄需要處理的參數在參數列表中的起始位置。緊跟在後面的是一系列對應的完成參數預處理的方法句柄。方法句柄與它要處理的參數是一一對應的。如果希望跳過某些參數不進行處理,可以使用null作爲方法句柄的值。在進行預處理的時候,要注意預處理方法句柄和原始方法句柄之間的類型匹配。如果預處理方法句柄用於對某個參數進行處理,那麼該方法句柄只能有一個參數,而且參數的類型必須匹配所要處理的參數的類型;其返回值類型需要匹配原始方法句柄中對應的參數類型。只有類型匹配,才能用方法句柄對實際傳入的參數進行預處理,再把預處理的結果作爲原始方法句柄調用時的參數來使用。

第四個方法foldArguments的作用與filterArguments很類似,都是用來對參數進行預處理的。不同之處在於,foldArguments對參數進行預處理之後的結果,不是替換掉原始的參數值,而是添加到原始參數列表的前面,作爲一個新的參數。當然,如果參數預處理的返回值是void,則不會添加新的參數。另外,參數預處理是由一個方法句柄完成的,而不是像filterArguments那樣可以由多個方法句柄來完成。這個方法句柄會負責處理根據它的類型確定的所有可用參數。下面先看一下具體的使用示例。代碼清單2-54中原始的方法句柄引用的是靜態方法targetMethod,而用來對參數進行預處理的方法句柄mhCombiner引用的是Math類中的max方法。變換之後的新方法句柄mhResult在被調用時,兩個參數3和4首先被傳遞給句柄mhCombiner所引用的Math.max方法,返回值是4。這個返回值被添加到原始調用參數列表的前面,即得到新的參數列表4、3、4。這個新的參數列表會在調用時被傳遞給原始方法句柄mhTarget所引用的targetMethod方法。

代碼清單2-54 foldArguments方法的使用示例
public static int targetMethod(int arg1, int arg2, int arg3) {
return arg1;
}

public void foldArguments() throws Throwable {
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodType typeCombiner = MethodType.methodType(int.class, int.class, int.class);
MethodHandle mhCombiner = lookup.findStatic(Math.class, "max", typeCombiner);
MethodType typeTarget = MethodType.methodType(int.class, int.class, int.class, int.class);
MethodHandle mhTarget = lookup.findStatic(Transform.class, "targetMethod", typeTarget);
MethodHandle mhResult = MethodHandles.foldArguments(mhTarget, mhCombiner);
int value = (int) mhResult.invoke(3, 4); //輸出爲4
}

進行參數預處理的方法句柄會根據其類型中參數的個數N,從實際調用的參數列表中獲取前面N個參數作爲它需要處理的參數。如果預處理的方法句柄有返回值,返回值的類型需要與原始方法句柄的第一個參數的類型匹配。這是因爲返回值會被作爲調用原始方法句柄時的第一個參數來使用。

第五個方法permuteArguments的作用是對調用時的參數順序進行重新排列,再傳遞給原始的方法句柄來完成調用。這種排列既可以是真正意義上的全排列,即所有的參數都在重新排列之後的順序中出現;也可以是僅出現部分參數,沒有出現的參數將被忽略;還可以重複某些參數,讓這些參數在實際調用中出現多次。代碼清單2-55給出了一個對參數進行完全排列的示例。代碼中的原始方法句柄mhCompare所引用的是Integer類中的compare方法。當使用參數3和4進行調用的時候,返回值是–1。通過permuteArguments方法把參數的排列順序進行顛倒,得到了新的方法句柄mhNew。再用同樣的參數調用方法句柄mhNew時,返回結果就變成了1,因爲傳遞給底層compare方法的實際調用參數變成了4和3。新方法句柄mhDuplicateArgs在通過permuteArguments方法進行變換的時候,重複了第二個參數,因此傳遞給底層compare方法的實際調用參數是4和4,返回的結果是0。

代碼清單2-55 permuteArguments方法的使用示例
public void permuteArguments() throws Throwable {
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodType type = MethodType.methodType(int.class, int.class, int.class);
MethodHandle mhCompare = lookup.findStatic(Integer.class, "compare", type);
int value = (int) mhCompare.invoke(3, 4); //值爲-1
MethodHandle mhNew = MethodHandles.permuteArguments(mhCompare, type, 1, 0);
value = (int) mhNew.invoke(3, 4); //值爲1
MethodHandle mhDuplicateArgs = MethodHandles.permuteArguments(mhCompare, type, 1, 1);
value = (int) mhDuplicateArgs.invoke(3, 4); // 值爲0
}

在這裏還要着重介紹一下permuteArguments方法的參數。第二個參數表示的是重新排列完成之後的新方法句柄的類型。緊接着的是多個用來表示新的排列順序的整數。這些整數的個數必須與原始句柄的參數個數相同。整數出現的位置及其值就表示了在排列順序上的對應關係。比如在上面的代碼中,創建方法句柄mhNew的第一個整數參數是1,這就表示調用原始方法句柄時的第一個參數的值實際上是調用新方法句柄時的第二個參數(編號從0開始,1表示第二個)。

第六個方法catchException與原始方法句柄調用時的異常處理有關。可以通過該方法爲原始方法句柄指定處理特定異常的方法句柄。如果原始方法句柄的調用正常完成,則返回其結果;如果出現了特定的異常,則處理異常的方法句柄會被調用。通過該方法可以實現通用的異常處理邏輯。可以對程序中可能出現的異常都提供一個進行處理的方法句柄,再通過catchException方法來封裝原始的方法句柄。

如代碼清單2-56所示,原始的方法句柄mhParseInt所引用的是Integer類中的parseInt方法,這個方法在字符串無法被解析成數字時會拋出java.lang.Number-FormatException。用來進行異常處理的方法句柄是mhHandler,它引用了當前類中的handleException方法。通過catchException得到的新方法句柄mh在被調用時,如果拋出了NumberFormatException,則會調用handleException方法。

代碼清單2-56 catchException方法的使用示例
public int handleException(Exception e, String str) {
System.out.println(e.getMessage());
return 0;
}

public void catchExceptions() throws Throwable {
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodType typeTarget = MethodType.methodType(int.class, String.class);
MethodHandle mhParseInt = lookup.findStatic(Integer.class, "parseInt", typeTarget);
MethodType typeHandler = MethodType.methodType(int.class, Exception.class, String.class);
MethodHandle mhHandler = lookup.findVirtual(Transform.class, "handleException", typeHandler).bindTo(this);
MethodHandle mh = MethodHandles.catchException(mhParseInt, NumberFormatException.class, mhHandler);
mh.invoke("Hello");
}

在這裏需要注意幾個細節:原始方法句柄和異常處理方法句柄的返回值類型必須是相同的,這是因爲當產生異常的時候,異常處理方法句柄的返回值會作爲調用的結果;而在兩個方法句柄的參數方面,異常處理方法句柄的第一個參數是它所處理的異常類型,其他參數與原始方法句柄的參數相同。在異常處理方法句柄被調用的時候,其對應的底層方法可以得到原始方法句柄調用時的實際參數值。在上面的例子中,當handleException方法被調用的時候,參數e的值是NumberFormatException類的對象,參數str的值是原始的調用值“Hello”;在獲得異常處理方法句柄的時候,使用了bindTo方法。這是因爲通過findVirtual找到的方法句柄的第一個參數類型表示的是方法調用的接收者,這與catchException要求的第一個參數必須是異常類型的約束不相符,因此通過bindTo方法來爲第一個參數預先綁定值。這樣就可以得到所需的正確的方法句柄。當然,如果異常處理方法句柄所引用的是靜態方法,就不存在這個問題。

最後一個在對方法句柄進行變換時與參數相關的方法是guardWithTest。這個方法可以實現在方法句柄這個層次上的條件判斷的語義,相當於if-else語句。使用guardWithTest時需要提供3個不同的方法句柄:第一個方法句柄用來進行條件判斷,而剩下的兩個方法句柄則分別在條件成立和不成立的時候被調用。用來進行條件判斷的方法句柄的返回值類型必須是布爾型,而另外兩個方法句柄的類型則必須一致,同時也是生成的新方法句柄的類型。

如代碼清單2-57所示,進行條件判斷的方法句柄mhTest引用的是靜態guardTest方法,在條件成立和不成立的時候調用的方法句柄則分別引用了Math類中的max方法和min方法。由於guardTest方法的返回值是隨機爲true或false的,所以兩個方法句柄的調用也是隨機選擇的。

代碼清單2-57 guardWithTest方法的使用示例
public static boolean guardTest() {
return Math.random() > 0.5;
}

public void guardWithTest() throws Throwable {
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle mhTest = lookup.findStatic(Transform.class, "guardTest", MethodType.methodType(boolean.class));
MethodType type = MethodType.methodType(int.class, int.class, int.class);
MethodHandle mhTarget = lookup.findStatic(Math.class, "max", type);
MethodHandle mhFallback = lookup.findStatic(Math.class, "min", type);
MethodHandle mh = MethodHandles.guardWithTest(mhTest, mhTarget, mhFallback);
int value = (int) mh.invoke(3, 5); //值隨機爲3或5
}

除了可以在變換的時候對方法句柄的參數進行處理之外,還可以對方法句柄被調用後的返回值進行修改。對返回值進行處理是通過filterReturnValue方法來實現的。原始的方法句柄被調用之後的結果會被傳遞給另外一個方法句柄進行再次處理,處理之後的結果被返回給調用者。代碼清單2-58展示了filterReturnValue的用法。原始的方法句柄mhSubstring所引用的是String類的substring方法,對返回值進行處理的方法句柄mhUpperCase所引用的是String類的toUpperCase方法。通過filterReturnValue方法得到的新方法句柄的運行效果是將調用substring得到的子字符串轉換成大寫的形式。

代碼清單2-58 filterReturnValue方法的使用示例
public void filterReturnValue() throws Throwable {
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle mhSubstring = lookup.findVirtual(String.class, "substring", MethodType.methodType(String.class, int.class));
MethodHandle mhUpperCase = lookup.findVirtual(String.class, "toUpperCase", MethodType.methodType(String.class));
MethodHandle mh = MethodHandles.filterReturnValue(mhSubstring, mhUpperCase);
String str = (String) mh.invoke("Hello World", 5); //輸出 WORLD
}

7.特殊方法句柄

在有些情況下,可能會需要對一組類型相同的方法句柄進行同樣的變換操作。這個時候與其對所有的方法句柄都進行重複變換,不如創建出一個可以用來調用其他方法句柄的方法句柄。這種特殊的方法句柄的invoke方法或invokeExact方法被調用的時候,可以指定另外一個類型匹配的方法句柄作爲實際調用的方法句柄。因爲調用方法句柄時可以使用invoke和invokeExact兩種方法,對應有兩種創建這種特殊的方法句柄的方式,分別通過MethodHandles類的invoker和exactInvoker實現。兩個方法都接受一個MethodType對象作爲被調用的方法句柄的類型參數,兩者的區別只在於調用時候的行爲是類似於invoke還是invokeExact。

代碼清單2-59給出了invoker方法的使用示例。首先invoker方法句柄可以調用的方法句柄類型的返回值類型爲String,加上3個類型分別爲Object、int和int的參數。兩個被調用的方法句柄,其中一個引用的是String類中的substring方法,另外一個引用的是當前類中的testMethod方法。這兩個方法都可以通過invoke方法來正確調用。

代碼清單2-59 invoker方法的使用示例
public void invoker() throws Throwable {
MethodType typeInvoker = MethodType.methodType(String.class, Object.class, int.class, int.class);
MethodHandle invoker = MethodHandles.invoker(typeInvoker);
MethodType typeFind = MethodType.methodType(String.class, int.class, int.class);
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle mh1 = lookup.findVirtual(String.class, "substring", typeFind);
MethodHandle mh2 = lookup.findVirtual(InvokerUsage.class, "testMethod", typeFind);
String result = (String) invoker.invoke(mh1, "Hello", 2, 3);
result = (String) invoker.invoke(mh2, this, 2, 3);
}

而exactInvoker的使用與invoker非常類似,這裏就不舉例說明了。

上面提到了使用invoker和exactInvoker的一個重要好處就是在對這個方法句柄進行變換之後,所得到的新方法句柄在調用其他方法句柄的時候,這些變換操作都會被自動地引用,而不需要對每個所調用的方法句柄再單獨應用。如代碼清單2-60所示,通過filterReturnValue爲通過exactInvoker得到的方法句柄添加變換操作,當調用方法句柄mh1的時候,這個變換會被自動應用,使作爲調用結果的字符串自動變成大寫形式。

代碼清單2-60 invoker和exactInvoker對方法句柄變換的影響
public void invokerTransform() throws Throwable {
MethodType typeInvoker = MethodType.methodType(String.class, String.class, int.class, int.class);
MethodHandle invoker = MethodHandles.exactInvoker(typeInvoker);
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle mhUpperCase = lookup.findVirtual(String.class, "toUpperCase", MethodType.methodType(String.class));
invoker = MethodHandles.filterReturnValue(invoker, mhUpperCase);
MethodType typeFind = MethodType.methodType(String.class, int.class, int.class);
MethodHandle mh1 = lookup.findVirtual(String.class, "substring", typeFind);
String result = (String) invoker.invoke(mh1, "Hello", 1, 4); //值爲“ELL”
}

通過invoker方法和exactInvoker方法得到的方法句柄被稱爲“元方法句柄”,具有調用其他方法句柄的能力。

8.使用方法句柄實現接口

2.3節介紹的動態代理機制可以在運行時爲多個接口動態創建實現類,並攔截通過接口進行的方法調用。方法句柄也具備動態實現一個接口的能力。這是通過java.lang.invoke.MethodHandleProxies類中的靜態方法asInterfaceInstance來實現的。不過通過方法句柄來實現接口所受的限制比較多。首先該接口必須是公開的,其次該接口只能包含一個名稱唯一的方法。這樣限制是因爲只有一個方法句柄用來處理方法調用。調用asInterfaceInstance方法時需要兩個參數,第一個參數是要實現的接口類,第二個參數是處理方法調用邏輯的方法句柄對象。方法的返回值是一個實現了該接口的對象。當調用接口的方法時,這個調用會被代理給方法句柄來完成。方法句柄的返回值作爲接口調用的返回值。接口的方法類型與方法句柄的類型必須是兼容的,否則會出現異常。

代碼清單2-61是使用方法句柄實現接口的示例。被代理的接口是java.lang.Runnable,其中僅包含一個run方法。實現接口的方法句柄引用的是當前類中的doSomething方法。在調用asInterfaceInstance之後得到的Runnable接口的實現對象被用來創建一個新的線程。該線程運行之後發現doSomething方法會被調用。這是由於當Runnable接口的run方法被調用的時候,方法句柄mh也會被調用。

代碼清單2-61 使用方法句柄實現接口的示例
public void doSomething() {
System.out.println("WORK");
}

public void useMethodHandleProxy() throws Throwable {
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle mh = lookup.findVirtual(UseMethodHandleProxies.class, "doSomething", MethodType.methodType(void.class));
mh = mh.bindTo(this);
Runnable runnable = MethodHandleProxies.asInterfaceInstance(Runnable.class, mh);
new Thread(runnable).start();
}

通過方法句柄來實現接口的優勢在於不需要新建額外的Java類,只需要複用已有的方法即可。在上面的示例中,任何已有的不帶參數和返回值的方法都可以用來實現Runnable接口。需要注意的是,要求接口所包含的方法的名稱唯一,不考慮Object類中的方法。實際的方法個數可能不止一個,可能包含同一方法的不同重載形式。

9.訪問控制權限

在通過查找已有類中的方法得到方法句柄時,要受限於Java語言中已有的訪問控制權限。方法句柄與反射API在訪問控制權限上的一個重要區別在於,在每次調用反射API的Method類的invoke方法的時候都需要檢查訪問控制權限,而方法句柄只在查找的時候需要進行檢查。只要在查找過程中不出現問題,方法句柄在使用中就不會出現與訪問控制權限相關的問題。這種實現方式也使方法句柄在調用時的性能要優於Method類。

之前介紹過,通過MethodHandles.Lookup類的方法可以查找類中已有的方法以得到MethodHandle對象。而MethodHandles.Lookup類的對象本身則是通過MethodHandles類的靜態方法lookup得到的。在Lookup對象被創建的時候,會記錄下當前所在的類(稱爲查找類)。只要查找類能夠訪問某個方法或域,就可以通過Lookup的方法來查找到對應的方法句柄。代碼清單2-62給出了一個訪問控制權限相關的示例。AccessControl類中的accessControl方法返回了引用其中私有方法privateMethod的方法句柄。由於當前查找類可以訪問該私有方法,因此查找過程是成功的。其他類通過調用accessControl得到的方法句柄就可以調用這個私有方法。雖然其他類不能直接訪問AccessControl類中的私有方法,但是在調用方法句柄的時候不會進行訪問控制權限檢查,因此對方法句柄的調用可以成功進行。

代碼清單2-62 方法句柄查找時的訪問控制權限
public class AccessControl {
private void privateMethod() {
System.out.println("PRIVATE");
}

public MethodHandle accessControl() throws Throwable {
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle mh = lookup.findSpecial(AccessControl.class, "privateMethod", MethodType.methodType(void.class), AccessControl.class);
mh = mh.bindTo(this);
return mh;
}
}

10. 交換點

交換點是在多線程環境下控制方法句柄的一個開關。這個開關只有兩個狀態:有效和無效。交換點初始時處於有效狀態,一旦從有效狀態變到無效狀態,就無法再繼續改變狀態。也就是說,只允許發生一次狀態改變。這種狀態變化是全局和即時生效的。使用同一個交換點的多個線程會即時觀察到狀態變化。交換點用java.lang.invoke.SwitchPoint類來表示。通過SwitchPoint對象的guardWithTest方法可以設置在交換點的不同狀態下調用不同的方法句柄。這個方法的作用類似於MethodHandles類中的guardWithTest方法,只不過少了用來進行條件判斷的方法句柄,只有條件成立和不成立時分別調用的方法句柄。這是因爲選擇哪個方法句柄來執行是由交換點的有效狀態來決定的,不需要額外的條件判斷。

在代碼清單2-63中,在調用guardWithTest方法的時候指定在交換點有效的時候調用方法句柄mhMin,而在無效的時候則調用mhMax。guardWithTest方法的返回值是一個新的方法句柄mhNew。交換點在初始時處於有效狀態,因此mhNew在第一次調用時使用的是mhMin,結果爲3。在通過invalidateAll方法把交換點設成無效狀態之後,再次調用mhNew時實際調用的方法句柄就變成了mhMax,結果爲4。

代碼清單2-63 交換點的使用示例
public void useSwitchPoint() throws Throwable {
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodType type = MethodType.methodType(int.class, int.class, int.class);
MethodHandle mhMax = lookup.findStatic(Math.class, "max", type);
MethodHandle mhMin = lookup.findStatic(Math.class, "min", type);
SwitchPoint sp = new SwitchPoint();
MethodHandle mhNew = sp.guardWithTest(mhMin, mhMax);
mhNew.invoke(3, 4); //值爲3
SwitchPoint.invalidateAll(new SwitchPoint[] {sp});
mhNew.invoke(3, 4); //值爲4
}

交換點的一個重要作用是在多線程環境下使用,可以在多個線程中共享同一個交換點對象。當某個線程的交換點狀態改變之後,其他線程所使用的guardWithTest方法返回的方法句柄的調用行爲就會發生變化。

11.使用方法句柄進行函數式編程

通過上面章節對方法句柄的詳細介紹可以看出,方法句柄是一個非常靈活的對方法進行操作的輕量級結構。方法句柄的作用類似於在某些語言中出現的函數指針(function pointer)。在程序中,方法句柄可以在對象之間自由傳遞,不受訪問控制權限的限制。方法句柄的這種特性,使得在Java語言中也可以進行函數式編程。下面通過幾個具體的示例來進行說明。

第一個示例是對數組進行操作。數組作爲一個常見的數據結構,有的編程語言提供了對它進行復雜操作的功能。這些功能中比較常見的是forEach、map和reduce操作等。這些操作的語義並不複雜,forEach是對數組中的每個元素都依次執行某個操作,而map則是把原始數組按照一定的轉換過程變成一個新的數組,reduce是把一個數組按照某種規則變成單個元素。這些操作在其他語言中可能比較好實現,而在Java語言中,則需要引入一些接口,由此帶來的是繁瑣的實現和冗餘的代碼。有了方法句柄之後,這個實現就變得簡單多了。代碼清單2-64給出了使用方法句柄的forEach、map和reduce方法的實現。對數組中元素的處理是由一個方法句柄來完成的。對這個方法句柄只有類型的要求,並不限制它所引用的底層方法所在的類或名稱。

代碼清單2-64 使用方法句柄實現數組操作的示例
private static final MethodType typeCallback = MethodType.methodType(Object.class, Object.class, int.class);

public static void forEach(Object[] array, MethodHandle handle) throws Throwable {
for (int i = 0, len = array.length; i < len; i++) {
handle.invoke(array[i], i);
}
}

public static Object[] map(Object[] array, MethodHandle handle) throws Throwable {
Object[] result = new Object[array.length];
for (int i = 0, len = array.length; i < len; i++) {
result[i] = handle.invoke(array[i], i);
}
return result;
}

public static Object reduce(Object[] array, Object initalValue, MethodHandle handle) throws Throwable {
Object result = initalValue;
for (int i = 0, len = array.length; i < len; i++) {
result = handle.invoke(result, array[i]);
}
return result;
}

第二個例子是方法的柯里化(currying)。柯里化的含義是對一個方法的參數值進行預先設置之後,得到一個新的方法。比如一個做加法運算的方法,本來有兩個參數,通過柯里化把其中一個參數的值設爲5之後,得到的新方法就只有一個參數。新方法的運行結果是用5加上這個唯一的參數的值。通過MethodHandles類中的insertArguments方法可以很容易地實現方法句柄的柯里化。代碼清單2-65給出了相關的實現。方法curry負責把一個方法句柄的第一個參數的值設爲指定值;add方法就是一般的加法操作;add5方法對引用add的方法句柄進行柯里化,得到新的方法句柄,再調用此方法句柄。

代碼清單2-65 使用方法句柄實現的柯里化
public static MethodHandle curry(MethodHandle handle, int value) {
return MethodHandles.insertArguments(handle, 0, value);
}

public static int add(int a, int b) {
return a + b;
}

public static int add5(int a) throws Throwable {
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodType type = MethodType.methodType(int.class, int.class, int.class);
MethodHandle mhAdd = lookup.findStatic(Curry.class, "add", type);
MethodHandle mh = curry(mhAdd, 5);
return (int) mh.invoke(a);
}

上面給出的這兩個示例所實現的功能雖然比較簡單,但是反映出了方法句柄在使用時的極大靈活性。配合方法句柄支持的變換操作,可以實現很多有趣和實用的功能。




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章