淺析方法句柄


方法句柄

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);
    }
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM