我一直听说所有添加到 JVM 的新特性很酷,其中一项很酷的特性是调用动态。我想知道它是什么以及它如何使 Java 中的反射式编程更容易或更好?
我一直听说所有添加到 JVM 的新特性很酷,其中一项很酷的特性是调用动态。我想知道它是什么以及它如何使 Java 中的反射式编程更容易或更好?
作为我的Java Records文章的一部分,我阐述了 Invoke Dynamic 背后的动机。让我们从 Indy 的粗略定义开始。
Invoke Dynamic(也称为Indy)是JSR 292 的一部分,旨在增强 JVM 对动态类型语言的支持。在 Java 7 中首次发布之后,invokedynamic
操作码及其java.lang.invoke
行李被 JRuby 等基于 JVM 的动态语言广泛使用。
尽管 indy 专门设计用于增强动态语言支持,但它提供的远不止这些。事实上,它适用于语言设计者需要任何形式的动态性的任何地方,从动态类型的杂技到动态策略!
例如,Java 8 Lambda 表达式实际上是使用 实现的invokedynamic
,尽管 Java 是一种静态类型语言!
很长一段时间,JVM 确实支持四种方法调用类型:invokestatic
调用静态方法、invokeinterface
调用接口方法、invokespecial
调用构造函数super()
或私有方法和invokevirtual
调用实例方法。
尽管存在差异,但这些调用类型具有一个共同特征:我们无法用我们自己的逻辑来丰富它们。相反,invokedynamic
使我们能够以任何我们想要的方式引导调用过程。然后 JVM 负责直接调用 Bootstrapped 方法。
JVM 第一次看到一条invokedynamic
指令时,它会调用一个特殊的静态方法,称为Bootstrap Method。bootstrap 方法是我们编写的一段 Java 代码,用于准备实际要调用的逻辑:
然后 bootstrap 方法返回 的一个实例java.lang.invoke.CallSite
。这CallSite
包含对实际方法的引用,即MethodHandle
。
从现在开始,JVM 每次invokedynamic
再次看到这条指令时,都会跳过慢路径,直接调用底层的可执行文件。除非发生某些变化,否则 JVM 会继续跳过慢速路径。
Java 14Records
提供了一种很好的紧凑语法来声明应该是哑数据持有者的类。
考虑这个简单的记录:
public record Range(int min, int max) {}
此示例的字节码类似于:
Compiled from "Range.java"
public java.lang.String toString();
descriptor: ()Ljava/lang/String;
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokedynamic #18, 0 // InvokeDynamic #0:toString:(LRange;)Ljava/lang/String;
6: areturn
在其Bootstrap 方法表中:
BootstrapMethods:
0: #41 REF_invokeStatic java/lang/runtime/ObjectMethods.bootstrap:
(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;
Ljava/lang/invoke/TypeDescriptor;Ljava/lang/Class;
Ljava/lang/String;[Ljava/lang/invoke/MethodHandle;)Ljava/lang/Object;
Method arguments:
#8 Range
#48 min;max
#50 REF_getField Range.min:I
#51 REF_getField Range.max:I
因此,Records的bootstrap方法被调用bootstrap
,它驻留在java.lang.runtime.ObjectMethods
类中。如您所见,此引导方法需要以下参数:
MethodHandles.Lookup
表示查找上下文的实例(Ljava/lang/invoke/MethodHandles$Lookup
部分)。toString
、equals
、hashCode
等)。例如,当值为 时toString
,bootstrap 将返回一个ConstantCallSite
(CallSite
永远不会改变的),它指向toString
这个特定记录的实际实现。TypeDescriptor
的方法(Ljava/lang/invoke/TypeDescriptor
部分)。Class<?>
表示 Record 类的类型。这是
Class<Range>
在这种情况下。min;max
.MethodHandle
每个组件一个。通过这种方式,bootstrap 方法可以MethodHandle
基于此特定方法实现的组件创建一个。该invokedynamic
指令将所有这些参数传递给 bootstrap 方法。反过来,Bootstrap 方法返回 的一个实例ConstantCallSite
。这ConstantCallSite
是对请求的方法实现的引用,例如toString
。
与反射 API 不同,该java.lang.invoke
API 非常高效,因为 JVM 可以完全看透所有调用。因此,JVM 可能会应用各种优化,只要我们尽可能避免慢速路径!
除了效率论点之外,该invokedynamic
方法由于其简单性而更加可靠且不易损坏。
此外,为 Java Records 生成的字节码与属性的数量无关。因此,更少的字节码和更快的启动时间。
最后,让我们假设一个新版本的 Java 包括一个新的和更有效的引导方法实现。使用invokedynamic
,我们的应用程序无需重新编译即可利用这一改进。这样我们就有了某种前向二进制兼容性。另外,这就是我们所说的动态策略!
除了 Java 记录之外,动态调用还用于实现以下功能:
LambdaMetafactory
StringConcatFactory
前段时间,C#添加了一个很酷的特性,C#中的动态语法
Object obj = ...; // no static type available
dynamic duck = obj;
duck.quack(); // or any method. no compiler checking.
将其视为反射方法调用的语法糖。它可以有非常有趣的应用。见http://www.infoq.com/presentations/Statically-Dynamic-Typing-Neal-Gafter
负责 C# 动态类型的 Neal Gafter 刚刚从 SUN 叛逃到 MS。所以认为 SUN 内部也讨论过同样的事情并不是没有道理的。
我记得在那之后不久,一些 Java 家伙宣布了类似的东西
InvokeDynamic duck = obj;
duck.quack();
不幸的是,该功能在 Java 7 中找不到。非常失望。对于 Java 程序员来说,他们没有简单的方法可以invokedynamic
在他们的程序中利用。
在继续调用动态之前,有两个概念需要理解。
1. 静态与动态类型
静态- 在编译时执行类型检查(例如 Java)
动态- 在运行时执行类型检查(例如 JavaScript)
类型检查是验证程序类型安全的过程,即检查类和实例变量、方法参数、返回值和其他变量的类型信息。例如,Java 在编译时知道 int、String、..,而 JavaScript 中的对象类型只能在运行时确定
2. 强打字 vs. 弱打字
强- 指定对其操作(例如 Java)提供的值类型的限制
弱- 如果操作的参数具有不兼容的类型(例如 Visual Basic),则转换(强制转换)这些参数
知道 Java 是静态和弱类型的,如何在 JVM 上实现动态和强类型语言?
invokedynamic 实现了一个运行时系统,该系统可以在程序编译后选择最合适的方法或函数实现。
示例: 具有 (a + b) 并且在编译时对变量 a,b 一无所知,invokedynamic 在运行时将此操作映射到 Java 中最合适的方法。例如,如果结果 a,b 是字符串,则调用 method(String a, String b)。如果结果 a,b 是整数,则调用 method(int a, int b)。
invokedynamic 是在 Java 7 中引入的。
这是一个新的 JVM 指令,它允许编译器生成调用方法的代码,这些代码比以前可能的规范更宽松——如果你知道什么是“鸭子类型”,invokedynamic 基本上允许鸭子类型。作为 Java 程序员,您能用它做的事情并不多;但是,如果您是工具创建者,则可以使用它来构建更灵活、更高效的基于 JVM 的语言。这是一篇非常棒的博客文章,提供了很多细节。