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如下:
上面图里的这些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");
}
}
从上面的代码,我们可以看到MethodHandles
,MethodHandle
,Lookup
,MethodType
这几个classes的使用方法。实际上这些classes提供了一种反射的机制来动态地访问和读取代码中的methods并进行调用。
有的同学可能要问,我们在更早版本的JDK里面不是有很多类反射的相关工具吗?是这样的,但是在工程世界里,新的功能往往要配合更加专门化,更加趁手的工具才行。比如上面这个”MethodType”类,它可以一次性封装一个method的所有参数类型和返回类型,这样使用起来就非常方便。
因此,我们可以把invokedynamic
指令看成是一个入口,真正干活的是Java层面的这些classes,而JVM层面除了要提供invokedynamic
这个指令,还要提供一个入口机制来把代码层和执行层连接起来。
P.S.
在InvokeDynamic 101
1这篇文章里,作者提供了一个完整的,使用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这个平台上做到。
-
http://www.javaworld.com/article/2860079/learn-java/invokedynamic-101.html?page=2 ↩