1. 程式人生 > >關於java.lang.invoke包的解釋

關於java.lang.invoke包的解釋

尊重原版:http://blog.csdn.net/zhangrongchao_/article/details/41603887

方法控制代碼(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);
}

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