淺析方法控制程式碼

Cruel_King發表於2020-10-22

方法控制程式碼

JKD 7 中引入了java.lang.invoke包,即方法控制程式碼,是反射的輕量級實現,它的作用是間接呼叫方法 ,方法控制程式碼中首先涉及到兩個重要的類,MethodHandle和MethodType

1. MethodHandle

它是對最終呼叫方法的"引用",類似於C++中的函式指標,或者說,它是一個有能力安全呼叫方法的物件。方法控制程式碼類似於反射中的Method類,他們本質上都是模擬方法呼叫,但是Reflection是在模擬Java程式碼層次的方法呼叫,而MethodHandle是在模擬位元組碼層次的方法呼叫,在MethodHandles.Lookup上的3個方法findStatic(),findVirtual(),findSpecial()正是為了對應invokestatic,invokevirtual(以及invokeinterface)和invokespecial這幾條位元組碼指令的執行許可權校驗行為。由於方法控制程式碼是對位元組碼的方法指令呼叫的模擬,那理論上虛擬機器在這方法做的各種優化(如方法內聯),在MethodHandle上也應當可以採用類似思路去支援,因此方法控制程式碼功能更強大靈活,MethodHandle可以服務於JVM上的所有語言

2. MethodType

它是表示方法簽名型別的不可變物件。每個方法控制程式碼都有一個MethodType例項,用來指明被呼叫方法的返回型別和引數型別。它的型別完全由返回型別和引數型別確定,而與它所引用的底層的方法的名稱和所在的類沒有關係。舉個例子,String類的length()方法和Integer類的intValue()方法的方法控制程式碼的型別就是一樣的,因為這兩個方法都沒有引數,而返回值都是int,則我們可以通過下面語句獲取同一個方法型別:MethodType mt = MethodType.methodType(int.class);
MethodType的物件例項只能通過MethodType類中的靜態工廠方法來建立,而且MethodType類的所有物件例項都是不可變的,類似於String類。如果修改了MethodType例項中的資訊,就會生成另外一個MethodType例項

3. 使用方法控制程式碼的簡單案例
public class MethodHandleTest {

    public MethodHandle getHandler() {
        MethodHandle mh = null;
        MethodType mt = MethodType.methodType(String.class, int.class, int.class);
        MethodHandles.Lookup lk = MethodHandles.lookup();

        try {
            mh = lk.findVirtual(String.class, "substring", mt);
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return mh;
    }

    public static void main(String[] args) throws Throwable {
        MethodHandle mh = new MethodHandleTest().getHandler();
        String str = "Hello World!";

        Object result1 = mh.invoke(str, 1, 3);
        Object result2 = (String) mh.invokeExact(str, 1, 3);

        /**
         * 下面這行程式碼在執行時會報錯,因為方法型別為String.class, int.class, int.class
         * 而下面這行程式碼的返回型別為Object,與申明中為String不符
         * 下面這行程式碼其中第二個引數型別為Integer,與宣告中為int不符,則型別適配不符合,系統報錯
         */
//        Object result3 = mh.invokeExact(str, new Integer(1), 3);

        System.out.println("result 1: " + result1);
        System.out.println("result 2: " + result2);
    }
    
}

上述程式碼的輸出都是 el

4. 方法控制程式碼的呼叫過程
  1. 先通過MethodType的靜態工廠方法生成一個包含方法返回型別,方法引數型別的方法型別,也就是MethodType mt = MethodType.methodType(String.class, int.class, int.class)(這裡假設呼叫方法是String類的substring(int, int)方法)
  2. 然後,獲取方法控制程式碼要用到的Lookup物件,例如上述程式碼中的 lk例項,這個物件可以提供其所在環境中任何可見方法的方法控制程式碼。我們可以把他想象成包含某個類物件的方法成員,方法的容器,通過 lk.findVirtual(String.class, "substring", mt); 具體鎖定String類中的名字為"substring",且方法型別為mt的方法,作為方法控制程式碼返回,總而言之,要想從lookup物件中得到方法控制程式碼,需要給出三個引數,分別為,持有所需方法的類,方法的名稱,以及跟方法相匹配的方法型別
  3. 最後,獲取到方法控制程式碼之後,我們就可以通過方法控制程式碼來呼叫底層的方法。方法控制程式碼提供兩個方法呼叫底層方法,invoke()和invokeExact()方法。
5. invoke()方法和invokeExact()方法
  1. 二者的相同點

二者的引數列表是相同的,第一個引數為方法的接收物件,即是哪個物件執行這個方法,接下來的引數就是執行方法所需要的引數

需要注意的是,引數列表中的第一個引數(接收物件)可以通過以下方式省略,可以通過方法控制程式碼的bindTo()方法來繫結具體的接收物件,從而使得方法控制程式碼的呼叫和普通方法沒什麼區別

  • 關於bindTo()方法的程式碼示例
public class MethodHandleTest {

    static class ClassA {
        public void println(String s) {
            System.out.println(s);
        }
    }

    public static MethodHandle getPrintMH(Object receiver) throws Throwable {
        MethodType mt = MethodType.methodType(void.class, String.class);
        return lookup().findVirtual(receiver.getClass(), "println", mt).bindTo(receiver);
    }

    public static void main(String[] args) throws Throwable {
        Object obj = System.currentTimeMillis() % 2 == 0 ? System.out : new ClassA();
        getPrintMH(obj).invokeExact("HelloWorld!");
    }

}
  • 程式輸出結果總是 HelloWorld
  1. 二者的不同點

從名字上來看,明顯是invokeExact方法準確性更高,或者說要求更嚴格,invokeExact方法呼叫時要求嚴格的型別匹配,方法的返回值型別也在考慮範圍之內,如果上面註釋掉的程式碼一樣。如果把第二次呼叫中Object result2 = (String) mh.invokeExact(str, 1, 3); 中的強制型別轉換去掉的話,在呼叫的時候方法會認為返回值為Object型別而不是String型別,然後系統報錯

與invokeExact方法不同,invoke方法允許稍微鬆散的呼叫方式,它會嘗試在呼叫的時候進行返回值型別和引數型別的轉換工作,從而使得方法型別不完全相等或者返回值型別不同的方法呼叫會產生一個新的方法控制程式碼,來適配此次方法呼叫,轉換基本規則如下,假設方法控制程式碼原MethodType為S,新的適配方法控制程式碼MethodType為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;
    前三點好理解,第4,5,6點我用一個例子說明一下
public class MethodHandleTest {
 
    public MethodHandle getHandler() {
        MethodHandle mh = null;
        MethodType mt = MethodType.methodType(void.class);
        MethodHandles.Lookup lk = MethodHandles.lookup();
         
        try {
            mh = lk.findVirtual(MethodHandleTest.class, "print", mt);
        } catch (Throwable e) {
            e.printStackTrace();
        }
         
        return mh;
    }
     
    public void print() {
        System.out.println("print");
    }
     
    public static void main(String[] args) throws Throwable {
        MethodHandleTest mht = new MethodHandleTest();
        MethodHandle mh = mht.getHandler();
         
        int result1 = (int) mh.invoke(mht);
        Object result2 = mh.invoke(mht);
         
        System.out.println("result 1:" + result1);
        System.out.println("result 2:" + result2);
    }
}

相關文章