Java 8 里面的双冒号语法和lambda一样,也是通过invokedynamic来实现的

本篇文章讲解Java 8中的双引号语言特性。

Java8里面引入了下面这种双冒号的语法形式:

import java.util.function.IntBinaryOperator;

public class DoubleColon {
    public static int testWith(IntBinaryOperator op, int left, int right) {
        return op.applyAsInt(left, right);
    }

    public static void main(String[] args) throws Exception {
        System.out.println(testWith(Math::max, 1, 2));
    }
}

上面代码里面的Math::max就是这个语法的使用。我们可以看到accept(...)方法里面接收的是IntBinaryOperator这个接口。那么上面这段代码是什么意思呢?首先我们看看IntBinaryOperator这个接口的代码:

package java.util.function;

/**
 * Represents an operation upon two {@code int}-valued operands and producing an
 * {@code int}-valued result.   This is the primitive type specialization of
 * {@link BinaryOperator} for {@code int}.
 *
 * <p>This is a <a href="package-summary.html">functional interface</a>
 * whose functional method is {@link #applyAsInt(int, int)}.
 *
 * @see BinaryOperator
 * @see IntUnaryOperator
 * @since 1.8
 */
@FunctionalInterface
public interface IntBinaryOperator {

    /**
     * Applies this operator to the given operands.
     *
     * @param left the first operand
     * @param right the second operand
     * @return the operator result
     */
    int applyAsInt(int left, int right);
}

从上面的代码中可以看到,这个接口只有一个applyAsInt(...)方法,它接收两个int类型的参数。因此,我们如果要实现上面的接口,可以这么写:


import java.util.function.IntBinaryOperator;

public class IntBinaryOperatorImpl implements IntBinaryOperator {
    @Override
    public int applyAsInt(int left, int right) {
        return Math.max(left, right);
    }
}

上面是一个实现了IntBinaryOperator.applyAsInt(...)方法的例子,里面的具体代码实现就一行,就是调用Math.max(...)方法并返回。如果要使用上面的class,在DoubleColon里这样写代码就可以:

testWith(new IntBinaryOperatorImpl());

从上面的代码里,我们可以看到IntBinaryOperatorImpl的类实例传递进了accept(...)方法。这样看完以后,我们会发现,其实上面的代码和我们的双冒号代码是一样的:

testWith(Math::max);

也就是说,Java8在编译上面的代码的时候,会检测到IntBinaryOperator这个接口只定义了一个方法,就是applyAsInt(int left, int right)。而Math.max(int a, int b)方法的参数和返回值的定义和applyAsInt(...)的定义是一样的,因此Math.max(...)方法就可以作为applyAsInt(...)的实现。

这样,Java在编译代码的时候,就可以帮助我们创建一个实现IntBinaryOperator接口的class,然后再使用Math.max(...)方法作为applyAsInt(...)的实现。

上面说的是一种可能的实现方法。但实际上Java不是像上面这样来简单实现的。实际上,Java8里面的双冒号语法和lambda一样,都是通过invokedynamic和相关的classes来实现的。

也就是说,为了实现这个双冒号语法以及lambda,或者更多更灵活的语法,Java这个语言平台干脆在VM层面加入了一条新的bytecode指令,就是invokedynamic。在这个指令的基础上加入了一大堆classe来支持这个指令。这些用来支持invokedynamic指令的classes如下:

/assets/jvm/relationship.png

上面图里的这些classes,我们可以写一个例子来学习它们的用法。代码如下:

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;

public class MethodHandleDemo {
    public static void main(String[] args) throws Throwable {
        MethodHandles.Lookup lookup = MethodHandles.lookup();
        MethodHandle handle =
                lookup.findStatic(MethodHandleDemo.class, "hello", MethodType.methodType(void.class));
        handle.invokeExact();
    }

    static void hello() {
        System.out.println("Hello");
    }
}

从上面的代码,我们可以看到MethodHandlesMethodHandleLookupMethodType这几个classes的使用方法。实际上这些classes提供了一种反射的机制来动态地访问和读取代码中的methods并进行调用。

有的同学可能要问,我们在更早版本的JDK里面不是有很多类反射的相关工具吗?是这样的,但是在工程世界里,新的功能往往要配合更加专门化,更加趁手的工具才行。比如上面这个”MethodType”类,它可以一次性封装一个method的所有参数类型和返回类型,这样使用起来就非常方便。

因此,我们可以把invokedynamic指令看成是一个入口,真正干活的是Java层面的这些classes,而JVM层面除了要提供invokedynamic这个指令,还要提供一个入口机制来把代码层和执行层连接起来。

P.S.

InvokeDynamic 1011这篇文章里,作者提供了一个完整的,使用invokedynamic指令的例子。首先是MHD class:

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;

public class MHD {
    public static void main(String[] args) throws Throwable {
        MethodHandles.Lookup lookup = MethodHandles.lookup();
        MethodHandle handle = lookup.findStatic(Math.class, "pow",
                MethodType.methodType(double.class,
                        double.class,
                        double.class));
        handle = MethodHandles.insertArguments(handle, 1, 10);
        System.out.printf("2^10 = %d%n", (int) (double) handle.invoke(2.0));
    }
}

上面这个class就是MethodHandleDemo的意思,原文里面的起名习惯太可怕不要在意。这个class就是通过invokedynamic配套的反射类们来调用Math.pow(...)方法并注入变量。

为什么要搞这么麻烦?因为JVM平台想为上层的语言实现提供一些更加灵活的机制,这样很多之前做不到的语言特性就可以在JVM这个平台上做到。

  1. http://www.javaworld.com/article/2860079/learn-java/invokedynamic-101.html?page=2