天天看點

關于Java的動态語言支援問題

最近在讀《深入了解Java虛拟機》第二版。看到第8章的動态類型語言支援的時候,發現一個有趣的問題。

前言

在《深入了解java虛拟機》第二版第8章中,主要内容是介紹JVM的位元組碼執行過程,在講解動态類型語言支援的時候引入了java.lang.invoke包,以下簡要介紹一下java.lang.invoke:

JDK 7 實作了 JSR 292 《Supporting Dynamically Typed Languages on the Java Platform》,新加入的 java.lang.invoke 包[3]是就是 JSR 292 的一個重要組成部分,這個包的主要目的是在之前單純依靠符号引用來确定調用的目标方法這條路之外,提供一種新的動态确定目标方法的機制,稱為 Method Handle。這個表達也不好懂?那不妨把 Method Handle 與 C/C++ 中的 Function Pointer,或者 C# 裡面的 Delegate 類比一下。舉個例子,如果我們要實作一個帶謂詞的排序函數,在 C/C++ 中常用做法是把謂詞定義為函數,用函數指針來把謂詞傳遞到排序方法,像這樣:

void sort(int list[], const int size, int (*compare)(int, int)) 

但 Java 語言中做不到這一點,沒有辦法單獨把一個函數作為參數進行傳遞。普遍的做法是設計一個帶有 compare() 方法的 Comparator 接口,以實作了這個接口的對象作為參數,例如 Collections.sort() 就是這樣定義的:

void sort(List list, Comparator c)

不過,在擁有 Method Handle 之後,Java 語言也可以擁有類似于函數指針或者委托的方法别名的工具了。下面代碼示範了 MethodHandle 的基本用途,無論 obj 是何種類型(臨時定義的 ClassA 抑或是實作 PrintStream 接口的實作類 System.out),都可以正确調用到 println() 方法。
public class MethodHandleTest {

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

    public static void main(String[] args) throws Throwable {
        Object obj = new ClassA();
        getPrintlnMH(obj).invokeExact("hello");
        Object s1 = (String) getSubHandler().invokeExact("hello world", 1, 3);
//        Object s2 = getSubHandler().invokeExact("hello world", 1, 3);
        /**
         * 上面這句方法執行時報錯,因為方法類型為String.class, int.class, int.class
         * 而傳回的類型為Object,與聲明中為String不符合
         * 其中第二個參數類型為Integer,與聲明中為int不符合,則類型适配不符合,系統報錯。
         */
        System.out.println(s1);
    }

    private static MethodHandle getPrintlnMH(Object receiver) throws Throwable {
        /*MethodType: 代表"方法類型",包含了方法的傳回值(methodType() 的第一個參數)和
         * 具體參數(methodType()第二個及以後的參數) */
        MethodType mt = MethodType.methodType(void.class, String.class);

        /*lookup方法用于在指定類中查找符合給定的方法名稱,方法類型,并且符合調用權限的方法句柄*/
        /*應為這裡調用的是一個虛方法,按照Java語言的規則,方法第一個參數是隐式的,代表該方法的接受這,
        也即是this指向的對象,這個參數以前是放在參數清單中傳遞的,而現在提供了bindTo()方法來完成這件事*/
        return MethodHandles.lookup()
                .findVirtual(receiver.getClass(), "println", mt)
                .bindTo(receiver);
    }

    public static MethodHandle getSubHandler() throws NoSuchMethodException, IllegalAccessException {
        MethodType mt = MethodType.methodType(String.class, int.class, int.class);
        return MethodHandles.lookup()
                .findVirtual(String.class, "substring", mt);
    }
}
           

附上MethodHandles.Lookup的findXXX方法說明

MethodHandle方法 位元組碼 描述
findStatic invokestatic 查找靜态方法
findSpecial invokespecial 查找執行個體構造方法,私有方法,父類方法。
findVirtual invokevirtual 查找所有的虛方法
invokeinterface 查找接口方法,會在運作時再确定一個實作此接口的對象。
findConstructor 查找構造方法
findGetter 查找非靜态變量getter方法
findSetter 查找非靜态變量setter方法
findStaticGetter 查找靜态變量getter方法
findStaticSetter 查找靜态變量setter方法

問題

書上關于動态分派内容中提到了這樣一個問題,簡要描述如下:

在Son類中,調用GrandFather的thinking方法,列印 I am grandFather。

Son類,GrandFather類定義如下:

class GrandFather {
        void thinking() {
            System.out.println("I am grandfather");
        }
    }

    class Father extends GrandFather {
        @Override
        void thinking() {
            System.out.println("I am father");
        }
    }

    class Son extends Father {
        @Override
        void thinking() {
            // 如何實作調用祖類(GrandFather)的thinking()方法?
            // 當然直接new一個GrandFather不算做本例的讨論内容
            // 使用反射的方式去做也可以,可以作為一個方案
        }
    }
           

解法

看了上邊的簡要說明,很自然的想法就是MethodType先描述下thinking方法,

之後使用MethodHandles.lookup()的findSpecial方法,在GrandFather上查找thinking方法進行執行。

書上的解法也類似,下面咱們就看看書上的解法。

public class MethodHandleTest {

    class GrandFather{
        void thinking(){
            System.out.println("I am grandFather!");
        }
    }
    class Father extends GrandFather{
        void thinking(){
            System.out.println("I am father!");
        }
    }
    class Son extends Father{
        void thinking() {
            //實作祖父類的thinking(),列印 I am grandFather
            MethodType mt=MethodType.methodType(void.class);
            try {
                MethodHandle md=MethodHandles.lookup().findSpecial(GrandFather.class, "thinking", mt,this.getClass());
                md.invoke(this);
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (Throwable e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) {
        MethodHandleTest.Son son=new MethodHandleTest().new Son();
        son.thinking();
    }
}
           

上述代碼在JDK1.7.0_09上運作正常,運作結果是I am grandFather

但是 該解法在JDK1.8下不行,運作結果是I am father

為什麼JDK1.8會有和JDK1.7有不一樣的表現?

帶着這個疑問我查閱了JDK8

規範說明

摘錄其中的一段文字說明如下:

A lookup class which needs to create method handles will call MethodHandles.lookup to create a factory for itself.
When the Lookup factory object is created, the identity of the lookup class is determined, 
and securely stored in the Lookup object. 
The lookup class (or its delegates) may then use factory methods on the Lookup object to create method handles 
for access-checked members. 
This includes all methods, constructors, and fields which are allowed to the lookup class, even private ones. 
           

翻譯如下:

需要建立method handles的查找類将調用MethodHandles.lookup為它自己建立一個工廠。
當該工廠對象被查找類建立後,查找類的辨別,安全資訊将存儲在其中。
查找類(或它的委托)将使用工廠方法在被查找對象上依據查找類的通路限制,建立method handles。
可建立的方法包括:查找類所有允許通路的所有方法、構造函數和字段,甚至是私有方法。
           

簡單說就是 :JDK1.8下MethodHandles.lookup是調用者敏感的,不同調用者通路權限不同,其結果也不同。

在本例中,在Son類中調用MethodHandles.lookup,受到Son限制,僅僅能通路到Father類的thinking。是以結果是:'I am father'

可以參照一下知乎

RednaxelaFX的回答

:

MethodHandle用于模拟invokespecial時,必須遵守跟Java位元組碼裡的invokespecial指令相同的限制——它隻能調用到傳給findSpecial()方法的最後一個參數(“specialCaller”)的直接父類的版本。invokespecial指令的規定可以參考JVM規範:Chapter 6. The Java Virtual Machine Instruction Set,不過這部分寫得比較“遞歸”是以不太直覺。findSpecial()還特别限制如果Lookup發現傳入的最後一個參數(“specialCaller”)跟目前類不一緻的話預設會馬上抛異常:jdk8u/jdk8u/jdk: e2117e30fb39 src/share/classes/java/lang/invoke/MethodHandles.java在這個例子裡,Son <: Father <: GrandFather,而Father與GrandFather類上都有自己的thinking()方法的實作,因而從Son出發查找就會找到其直接父類Father上的thinking(),即便傳給findSpecial()的第一個參數是GrandFather。請參考文檔:MethodHandles.Lookup (Java Platform SE 8 )-題主所參考的書給的例子不正确,可能是因為findSpecial()得到的MethodHandle的具體語義在JSR 292的設計過程中有被調整過。有一段時間findSpecial()得到的MethodHandle确實可以超越invokespecial的限制去調用到任意版本的虛方法,但這種行為很快就被認為是bug而修正了。
           

JDK1.8下的解法

public class CustomDynamicDispatch {

    class GrandFather {
        void thinking() {
            System.out.println("I am grandfather");
        }
    }

    class Father extends GrandFather {
        @Override
        void thinking() {
            System.out.println("I am father");
        }
    }

    class Son extends Father {
        @Override
        void thinking() {
            try {
                /*以下這段代碼是書上給的,在jdk1.7中成立,但是在1.8中不成立,理由可以見 https://my.oschina.net/floor/blog/1535062*/
//                MethodType mt = MethodType.methodType(void.class);
//                MethodHandles.lookup()
//                        .findSpecial(GrandFather.class, "thinking", mt, getClass())
//                        .invoke(this);
                /*下面給出在1.8的解決方案*/
                MethodType mt = MethodType.methodType(void.class);
                // 設定通路權限
                Field IMPL_LOOKUP = MethodHandles.Lookup.class.getDeclaredField("IMPL_LOOKUP");
                IMPL_LOOKUP.setAccessible(true);
                MethodHandles.Lookup lookup = (MethodHandles.Lookup) IMPL_LOOKUP.get(null);
                MethodHandle mh = lookup.findSpecial(GrandFather.class, "thinking", mt, GrandFather.class);
                mh.invoke(new Son());
            } catch (Throwable throwable) {
                throwable.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        (new CustomDynamicDispatch().new Son()).thinking();
    }

}
           

後記

學習完這一章有一個很大的疑惑就是這個java.lang.invoke包(下面簡稱invoke)和java.lang.reflect(下面簡稱reflect)的差別是啥,後來簡要整理了一下:

  • invoke服務于所有java虛拟機上的語言,reflect僅僅服務于java語言。
  • reflect在模拟Java代碼層次的調用,而invoke在模拟位元組碼層次的方法調用。
  • reflect是重量級,而invoke是輕量級。
  • invoke可以進行内聯優化,reflect完全沒有。

參考文檔

《深入了解Java虛拟機》第二版-第8章 知乎-RednaxelaFX的回答 Class MethodHandles.Lookup