关于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 查找所有的虚方法
findVirtual 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
关于Java的动态语言支持问题

但是 该解法在JDK1.8下不行,运行结果是I am father
关于Java的动态语言支持问题

为什么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





上一篇:centos6与centos7自动化安装mariadb脚本


下一篇:Java中PO,VO,POJO,DTO,DAO的基本概念