Java Lambda 表达式与 JVM 中的 Invoke Dynamic 简介
Posted 东海陈光剑
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java Lambda 表达式与 JVM 中的 Invoke Dynamic 简介相关的知识,希望对你有一定的参考价值。
1. 概述
Invoke Dynamic(也称为 Indy)是JSR 292 的一部分,旨在增强 JVM 对动态类型语言的支持。在 Java 7 中首次发布之后,invokedynamic 操作码被 JRuby 等基于 JVM 的动态语言甚至 Java 等静态类型语言广泛使用。
在本教程中,我们将揭开invokedynamic 的神秘面纱,看看它如何 帮助库和语言设计者实现多种形式的动态性。
2. 认识动态调用
让我们从一个简单的Stream API调用链开始:
public class Main {
public static void main(String[] args) {
long lengthyColors = List.of("Red", "Green", "Blue")
.stream().filter(c -> c.length() > 3).count();
}
}
起初,我们可能认为 Java 创建了一个从Predicate 派生的匿名内部类 ,然后将该实例传递给 filter 方法。 但是,我们错了。
2.1. 字节码
为了验证这个假设,我们可以看一下生成的字节码:
javap -c -p Main
// truncated
// class names are simplified for the sake of brevity
// for instance, Stream is actually java/util/stream/Stream
0: ldc #7 // String Red
2: ldc #9 // String Green
4: ldc #11 // String Blue
6: invokestatic #13 // InterfaceMethod List.of:(LObject;LObject;)LList;
9: invokeinterface #19, 1 // InterfaceMethod List.stream:()LStream;
14: invokedynamic #23, 0 // InvokeDynamic #0:test:()LPredicate;
19: invokeinterface #27, 2 // InterfaceMethod Stream.filter:(LPredicate;)LStream;
24: invokeinterface #33, 1 // InterfaceMethod Stream.count:()J
29: lstore_1
30: return
不管我们怎么想,没有匿名内部类,当然,没有人将此类类的实例传递给filter 方法。
令人惊讶的是,invokedynamic 指令以某种方式负责创建 Predicate 实例。
2.2. Lambda 特定方法
此外,Java 编译器还生成了以下看起来很有趣的静态方法:
private static boolean lambda$main$0(java.lang.String);
Code:
0: aload_0
1: invokevirtual #37 // Method java/lang/String.length:()I
4: iconst_3
5: if_icmple 12
8: iconst_1
9: goto 13
12: iconst_0
13: ireturn
此方法以 字符串 作为输入,然后执行以下步骤:
计算输入长度(invokevirtual on length)
将长度与常量 3 进行比较(if_icmple 和 iconst_3)
如果长度小于或等于 3,则返回 false
有趣的是,这实际上相当于我们传递给filter 方法的 lambda :
c -> c.length() > 3
因此,Java 不是匿名内部类,而是创建了一个特殊的静态方法,并通过调用动态以某种方式调用该方法 。
在本文中,我们将了解此调用在内部是如何工作的。但是,首先,让我们定义invokedynamic 试图解决的问题。
2.3. 问题
Java 7中之前,JVM只是有四个方法调用类型:invokevirtual 调用正常类方法, invokestatic 调用静态方法, invokeinterface 调用接口的方法,并 invokespecial 调用构造函数或私有方法。
尽管存在差异,但所有这些调用都有一个简单的特征:它们有一些预定义的步骤来完成每个方法调用,我们无法用我们的自定义行为来丰富这些步骤。
此限制有两种主要解决方法:一种是在编译时,另一种是在运行时。前者通常由Scala或Koltin 等语言使用,后者是 JRuby 等基于 JVM 的动态语言的首选解决方案。
运行时方法通常是基于反射的,因此效率低下。
另一方面,编译时解决方案通常依赖于编译时的代码生成。这种方法在运行时更有效。但是,它有点脆弱,并且可能会导致启动时间变慢,因为要处理的字节码更多。
现在我们对问题有了更好的理解,让我们看看解决方案在内部是如何工作的。
3. Under the Hood
invokedynamic让我们引导的方法调用过程中,我们想要的任何方式。也就是说,当JVM看到一个 invokedynamic 操作码的第一次,它调用被称为引导方法来初始化调用过程的特殊方法:
bootstrap 方法是我们为设置调用过程而编写的一段普通 Java 代码。因此,它可以包含任何逻辑。一旦引导方法正常完成,它应该返回一个CallSite实例 。 此 CallSite 封装了以下信息:
指向 JVM 应该执行的实际逻辑的指针。这应该表示为 MethodHandle。
表示返回的CallSite有效性的条件 。
从现在开始,每次 JVM 再次看到这个特定的操作码时,它都会跳过慢速路径并直接调用底层的可执行文件。此外,JVM 将继续跳过慢速路径,直到CallSite 中 的条件发生变化。
与反射 API 不同,JVM 可以完全透视MethodHandle并尝试优化它们,从而获得更好的性能。
3.1. Bootstrap 方法表
我们再看一下生成的 invokedynamic 字节码:
14: invokedynamic #23, 0 // InvokeDynamic #0:test:()Ljava/util/function/Predicate;
这意味着此特定指令应调用引导程序方法表中的第一个引导程序方法(#0 部分)。 此外,它还提到了一些传递给 bootstrap 方法的参数:
该测试 是在只有抽象方法 谓词
在()Ljava / util的/功能/谓词 表示在JVM的方法签名-该方法采用什么作为输入,并返回的一个实例谓词 接口
为了查看 lambda 示例的引导方法表,我们应该将-v 选项传递 给 javap:
javap -c -p -v Main
// truncated
// added new lines for brevity
BootstrapMethods:
0: #55 REF_invokeStatic java/lang/invoke/LambdaMetafactory.metafactory:
(Ljava/lang/invoke/MethodHandles$Lookup;
Ljava/lang/String;
Ljava/lang/invoke/MethodType;
Ljava/lang/invoke/MethodType;
Ljava/lang/invoke/MethodHandle;
Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
Method arguments:
#62 (Ljava/lang/Object;)Z
#64 REF_invokeStatic Main.lambda$main$0:(Ljava/lang/String;)Z
#67 (Ljava/lang/String;)Z
所有 lambda 的 bootstrap 方法是 LambdaMetafactory类中的元工厂 静态方法 。
与所有其他引导方法类似,这个方法至少需要三个参数,如下所示:
该Ljava /朗/调用/ MethodHandles $查找 参数表示的查找范围内 invokedynamic
所述 Ljava /郎/字符串 表示在调用位置的方法的名称-在该示例中,方法名是测试
该 Ljava /朗/调用/ MethodType 是呼叫站点的动态方法签名-在这种情况下,它的()Ljava / UTIL /功能/谓语
除了这三个参数之外,引导程序方法还可以选择接受一个或多个额外参数。在这个例子中,这些是额外的:
的 (Ljava /郎/对象;)z 是一个擦除方法签名接受的实例 对象 ,并返回一个 布尔值。
所述 REF_invokeStatic Main.lambda $主$ 0:(Ljava /郎/字符串;)z 是 MethodHandle 指向实际拉姆达逻辑。
的 (Ljava /郎/字符串;)z 是未经擦除的方法签名接受一个 字符串 并返回一个布尔值。
简而言之,JVM 会将所有需要的信息传递给 bootstrap 方法。反过来,Bootstrap 方法将使用该信息来创建合适的Predicate实例。 然后,JVM 会将该实例传递给filter 方法。
3.2. 不同类型 CallSite
一旦JVM第一次看到 这个例子中的invokedynamic ,它就会调用bootstrap方法。在撰写本文时,lambda bootstrap 方法将使用 InnerClassLambdaMetafactory在运行时为 lambda 生成内部类。
然后 bootstrap 方法将生成的内部类封装在称为 ConstantCallSite的特殊类型 CallSite 中 。 这种类型的 CallSite 在设置后永远不会改变。因此,在对每个 lambda 进行首次设置后,JVM 将始终使用快速路径直接调用 lambda 逻辑。
尽管这是最有效的调用动态类型 , 但它肯定不是唯一可用的选项。事实上,Java 提供了 MutableCallSite 和 VolatileCallSite 来适应更多的动态需求。
3.3. 好处
因此,为了实现 lambda 表达式,Java 不是在编译时创建匿名内部类,而是在运行时通过调用动态创建它们 。
有人可能会反对将内部类的生成推迟到运行时。但是,与简单的编译时解决方案相比, invokedynamic 方法有一些优势。
首先,JVM 在第一次使用 lambda 之前不会生成内部类。因此,在第一次 lambda 执行之前,我们不会为与内部类相关的额外占用空间付费。
此外,许多链接逻辑从字节码移出到引导方法。因此,在 invokedynamic 字节码通常比其他解决方案更小。较小的字节码可以提高启动速度。
假设较新版本的 Java 带有更有效的引导方法实现。然后我们的 invokedynamic 字节码可以利用这种改进而无需重新编译。这样我们就可以实现某种转发二进制兼容性。基本上,我们可以在不重新编译的情况下在不同的策略之间切换。
最后,用 Java 编写引导和链接逻辑通常比遍历 AST 生成复杂的字节码更容易。因此,invokedynamic 可以(主观上)不那么脆弱。
4. 例子
Lambda 表达式不是唯一的特性,Java 也不是唯一使用调用动态的语言。 在本节中,我们将熟悉动态调用的其他一些示例。
4.1. Java 14:记录
记录是Java 14中的一项新预览功能,它 提供了一种简洁的语法来声明应该是哑数据持有者的类。
这是一个简单的记录示例:
public record Color(String name, int code) {}
鉴于这个简单的单行,Java 编译器为访问器方法、toString、equals 和 hashcode生成适当的实现 。
为了实现 toString、equals 或 hashcode, Java 使用了 invokedynamic。 例如,equals 的字节码 如下:
public final boolean equals(java.lang.Object);
Code:
0: aload_0
1: aload_1
2: invokedynamic #27, 0 // InvokeDynamic #0:equals:(LColor;Ljava/lang/Object;)Z
7: ireturn
另一种解决方案是查找所有记录字段并在编译时根据这些字段生成 equals 逻辑。我们拥有的字段越多,字节码就越长。
相反,Java 在运行时调用引导程序方法来链接适当的实现。因此,无论字段数量如何,字节码长度都将保持不变。
仔细查看字节码表明引导方法是ObjectMethods#bootstrap:
BootstrapMethods:
0: #42 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 Color
#49 name;code
#51 REF_getField Color.name:Ljava/lang/String;
#52 REF_getField Color.code:I
4.2. Java 9:字符串连接
在 Java 9 之前,重要的字符串连接是使用StringBuilder实现的 。 作为 JEP 280 的一部分,字符串连接现在使用 invokedynamic。 例如,让我们将一个常量字符串与一个随机变量连接起来:
"random-" + ThreadLocalRandom.current().nextInt();
下面是这个例子的字节码的样子:
0: invokestatic #7 // Method ThreadLocalRandom.current:()LThreadLocalRandom;
3: invokevirtual #13 // Method ThreadLocalRandom.nextInt:()I
6: invokedynamic #17, 0 // InvokeDynamic #0:makeConcatWithConstants:(I)LString;
此外,字符串连接的引导方法驻留在 StringConcatFactory类中:
BootstrapMethods:
0: #30 REF_invokeStatic java/lang/invoke/StringConcatFactory.makeConcatWithConstants:
(Ljava/lang/invoke/MethodHandles$Lookup;
Ljava/lang/String;
Ljava/lang/invoke/MethodType;
Ljava/lang/String;
[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
Method arguments:
#36 random-\\u0001
5. 结论
在本文中,首先,我们熟悉了 indy 试图解决的问题。
然后,通过一个简单的 lambda 表达式示例,我们看到了invokedynamic 在内部是如何工作的。
最后,我们列举了最新版本的 Java 中的其他几个 indy 示例。
Deep Inside Lambda Expression
What does a lambda expression look like inside Java code and inside the JVM? It is obviously some type of value, and Java permits only two sorts of values: primitive types and object references. Lambdas are obviously not primitive types, so a lambda expression must therefore be some sort of expression that returns an object reference.
Let’s look at an example:
public class LambdaExample {
private static final String HELLO = "Hello World!";
public static void main(String[] args) throws Exception {
Runnable r = () -> System.out.println(HELLO);
Thread t = new Thread(r);
t.start();
t.join();
}
}
Programmers who are familiar with inner classes might guess that the lambda is really just syntactic sugar for an anonymous implementation of Runnable.
However, compiling the above class generates a single file: LambdaExample.class
. There is no additional class file for the inner class.
This means that lambdas are not inner classes; rather, they must be some other mechanism. In fact, decompiling the bytecode via javap -c -p
reveals two things. First is the fact that the lambda body has been compiled into a private static method that appears in the main class:
private static void lambda$main$0();
Code:
0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #9 // String Hello World!
5: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
You might guess that the signature of the private body method matches that of the lambda, and indeed this is the case. A lambda such as this
public class StringFunction {
public static final Function<String, Integer> fn = s -> s.length();
}
will produce a body method such as this, which takes a string and returns an integer, matching the signature of the interface method
private static java.lang.Integer lambda$static$0(java.lang.String);
Code:
0: aload_0
1: invokevirtual #2 // Method java/lang/String.length:()I
4: invokestatic #3 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
7: areturn
The second thing to notice about the bytecode is the form of the main method:
public static void main(java.lang.String[]) throws java.lang.Exception;
Code:
0: invokedynamic #2, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable;
5: astore_1
6: new #3 // class java/lang/Thread
9: dup
10: aload_1
11: invokespecial #4 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
14: astore_2
15: aload_2
16: invokevirtual #5 // Method java/lang/Thread.start:()V
19: aload_2
20: invokevirtual #6 // Method java/lang/Thread.join:()V
23: return
Notice that the bytecode begins with an invokedynamic
call. This opcode was added to Java with version 7 (and it is the only opcode ever added to JVM bytecode). I discussed method invocation in “Real-world bytecode Handling with ASM” and in “Understanding Java method invocation with invokedynamic” which you can read as companions to this article.
The most straightforward way to understand the invokedynamic
call in this code is to think of it as a call to an unusual form of the factory method. The method call returns an instance of some type that implements Runnable
. The exact type is not specified in the bytecode and it fundamentally does not matter.
The actual type does not exist at compile time and will be created on demand at runtime. To better explain this, I’ll discuss three mechanisms that work together to produce this capability: call sites, method handles, and bootstrapping.
Call sites
A location in the bytecode where a method invocation instruction occurs is known as a call site.
Java bytecode has traditionally had four opcodes that handle different cases of method invocation: static methods, “normal” invocation (a virtual call that may involve method overriding), interface lookup, and “special” invocation (for cases where override resolution is not required, such as superclass calls and private methods).
Dynamic invocation goes much further than that by offering a mechanism through which the decision about which method is actually called is made by the programmer, on a per-call site basis.
Here, invokedynamic
call sites are represented as CallSite
objects in the Java heap. This isn’t strange: Java has been doing similar things with the Reflection API since Java 1.1 with types such as Method
and, for that matter, Class
. Java has many dynamic behaviors at runtime, so there should be nothing surprising about the idea that Java is now modeling call sites as well as other runtime type information.
When the invokedynamic
instruction is reached, the JVM locates the corresponding call site object (or it creates one, if this call site has never been reached before). The call site object contains a method handle, which is an object that represents the method that I actually want to invoke.
The call site object is a necessary level of indirection, allowing the associated invocation target (that is, the method handle) to change over time.
There are three available subclasses of CallSite
(which is abstract): ConstantCallSite
, MutableCallSite
, and VolatileCallSite
. The base class has only package-private constructors, while the three subtypes have public constructors. This means that CallSite
cannot be directly subclassed by user code, but it is possible to subclass the subtypes. For example, the JRuby language uses invokedynamic
as part of its implementation and subclasses MutableCallSite
.
Note: Some invokedynamic
call sites are effectively just lazily computed, and the method they target will never change after they have been executed the first time. This is a very common use case for ConstantCallSite
, and this includes lambda expressions.
This means that a nonconstant call site can have many different method handles as its target over the lifetime of a program.
Method handles
Reflection is a powerful technique for doing runtime tricks, but it has a number of design flaws (hindsight is 20/20, of course), and it is definitely showing its age now. One key problem with reflection is performance, especially since reflective calls are difficult for the just-in-time (JIT) compiler to inline.
This is bad, because inlining is very important to JIT compilation in several ways, not the least of which is because it’s usually the first optimization applied and it opens the door to other techniques (such as escape analysis and dead code elimination).
A second problem is that reflective calls are linked every time the call site of Method.invoke()
is encountered. That means, for example, that security access checks are performed. This is very wasteful because the check will typically either succeed or fail on the first call, and if it succeeds, it will continue to do so for the life of the program. Yet, reflection does this linking over and over again. Thus, reflection incurs a lot of unnecessary cost by relinking and wasting CPU time.
To solve these problems (and others), Java 7 introduced a new API, java.lang.invoke
, which is often casually called method handles due to the name of the main class it introduced.
A method handle (MH) is Java’s version of a type-safe function pointer. It’s a way of referring to a method that the code might want to call, similar to a Method
object from Java reflection. The MH has an invoke()
method that actually executes the underlying method, in just the same way as reflection.
At one level, MHs are really just a more efficient reflection mechanism that’s closer to the metal; anything represented by an object from the Reflection API can be converted to an equivalent MH. For example, a reflective Method
object can be converted to an MH using Lookup.unreflect()
. The MHs that are created are usually a more efficient way to access the underlying methods.
MHs can be adapted, via helper methods in the MethodHandles
class, in a number of ways such as by composition and the partial binding of method arguments (currying).
Normally, method linkage requires exact matching of type descriptors. However, the invoke()
method on an MH has a special polymorphic signature that allows linkage to proceed regardless of the signature of the method being called.
At runtime, the signature at the invoke()
call site should look like you are calling the referenced method directly, which avoids type conversions and autoboxing costs that are typical with reflected calls.
Because Java is a statically typed language, the question arises as to how much type-safety can be preserved when such a fundamentally dynamic mechanism is used. The MH API addresses this by use of a type called MethodType
, which is an immutable representation of the arguments that a method takes: the signature of the method.
The internal implementation of MHs was changed during the lifetime of Java 8. The new implementation is called lambda forms, and it provided a dramatic performance improvement with MHs now being better than reflection for many use cases.
Bootstrapping
The first time each specific invokedynamic
call site is encountered in the bytecode instruction stream, the JVM doesn’t know which method it targets. In fact, there is no call site object associated with the instruction.
The call site needs to be bootstrapped, and the JVM achieves this by running a bootstrap method (BSM) to generate and return a call site object.
Each invokedynamic
call site has a BSM associated with it, which is stored in a separate area of the class file. These methods allow user code to programmatically determine linkage at runtime.
Decompiling an invokedynamic
call, such as that from my original example of a Runnable
, shows that it has this form:
0: invokedynamic #2, 0
And in the class file’s constant pool, notice that entry #2 is a constant of type CONSTANT_InvokeDynamic
. The relevant parts of the constant pool are
#2 = InvokeDynamic #0:#31
...
#31 = NameAndType #46:#47 // run:()Ljava/lang/Runnable;
#46 = Utf8 run
#47 = Utf8 ()Ljava/lang/Runnable;
The presence of 0 in the constant is a clue. Constant pool entries are numbered from 1, so the 0 reminds you that the actual BSM is located in another part of the class file.
For lambdas, the NameAndType
entry takes on a special form. The name is arbitrary, but the type signature contains some useful information.
The return type corresponds to the return type of the invokedynamic
factory; it is the target type of the lambda expression. Also, the argument list consists of the types of elements that are being captured by the lambda. In the case of a stateless lambda, the return type will always be empty. Only a Java closure will have arguments present.
A BSM takes at least three arguments and returns a CallSite
. The standard arguments are of these types:
MethodHandles.Lookup
: A lookup object on the class in which the call site occursString
: The name mentioned in theNameAndType
MethodType
: The resolved type descriptor of theNameAndType
Following these arguments are any additional arguments that are needed by the BSM. These are referred to as additional static arguments in the documentation.
The general case of BSMs allows an extremely flexible mechanism, and non-Java language implementers use this. However, the Java language does not provide a language-level construct for producing arbitrary invokedynamic
call sites.
For lambda expressions, the BSM takes a special form and to fully understand how the mechanism works, I will examine it more closely.
Decoding the lambda’s bootstrap method
Use the -v
argument to javap
to see the bootstrap methods. This is necessary because the bootstrap methods live in a special part of the class file and make references back into the main constant pool. For this simple Runnable
example, there is a single bootstrap method in that section:
BootstrapMethods:
0: #28 REF_invokeStatic java/lang/invoke/LambdaMetafactory.metafactory:
(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;
Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;
Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
Method arguments:
#29 ()V
#30 REF_invokeStatic LambdaExample.lambda$main$0:()V
#29 ()V
That is a bit hard to read, so let’s decode it.
The bootstrap method for this call site is entry #28 in the constant pool. This is an entry of type MethodHandle
(a constant pool type that was added to the standard in Java 7). Now let’s compare it to the case of the string function example:
0: #27 REF_invokeStatic java/lang/invoke/LambdaMetafactory.metafactory:
(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;
Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;
Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
Method arguments:
#28 (Ljava/lang/Object;)Ljava/lang/Object;
#29 REF_invokeStatic StringFunction.lambda$static$0:(Ljava/lang/String;)Ljava/lang/Integer;
#30 (Ljava/lang/String;)Ljava/lang/Integer;
The method handle that will be used as the BSM is the same static method LambdaMetafactory.metafactory( ... )
.
The part that has changed is the method arguments. These are the additional static arguments for lambda expressions, and there are three of them. They represent the lambda’s signature and the method handle for the actual final invocation target of the lambda: the lambda body. The third static argument is the erased form of the signature.
Let’s follow the code into java.lang.invoke
and see how the platform uses metafactories to dynamically spin the classes that actually implement the target types for the lambda expressions.
The lambda metafactories
The BSM makes a call to this static method, which ultimately returns a call site object. When the invokedynamic
instruction is executed, the method handle contained in the call site will return an instance of a class that implements the lambda’s target type.
The source code for the metafactory method is relatively simple:
public static CallSite metafactory(MethodHandles.Lookup caller,
String invokedName,
MethodType invokedType,
MethodType samMethodType,
MethodHandle implMethod,
MethodType instantiatedMethodType)
throws LambdaConversionException {
AbstractValidatingLambdaMetafactory mf;
mf = new InnerClassLambdaMetafactory(caller, invokedType,
invokedName, samMethodType,
implMethod, instantiatedMethodType,
false, EMPTY_CLASS_ARRAY, EMPTY_MT_ARRAY);
mf.validateMetafactoryArgs();
return mf.buildCallSite();
}
The lookup object corresponds to the context where the invokedynamic
instruction lives. In this case, that is the same class where the lambda was defined, so the lookup context will have the correct permissions to access the private method that the lambda body was compiled into.
The invoked name and type are provided by the VM and are implementation details. The final three parameters are the additional static arguments from the BSM.
In the current implementation, the metafactory delegates to code that uses an internal, shaded copy of the ASM bytecode libraries to spin up an inner class that implements the target type.
If the lambda does not capture any parameters from its enclosing scope, the resulting object is stateless, so the implementation optimizes by precomputing a single instance—effectively making the lambda’s implementation class a singleton:
jshell> Function<String, Integer> makeFn() {
...> return s -> s.length();
...> }
| created method makeFn()
jshell> var f1 = makeFn();
f1 ==> $Lambda$27/0x0000000800b8f440@533ddba
jshell> var f2 = makeFn();
f2 ==> $Lambda$27/0x0000000800b8f440@533ddba
jshell> var f3 = makeFn();
f3 ==> $Lambda$27/0x0000000800b8f440@533ddba
This is one reason why the documentation strongly discourages Java programmers from relying upon any form of identity semantics for lambdas.
Conclusion
This article explored the fine-grained details of exactly how the JVM implements support for lambda expressions. This is one of the more complex platform features you’ll encounter, because it is deep into language implementer territory.
Along the way, I’ve discussed invokedynamic
and the method handles API. These are two key techniques that are major parts of the modern JVM platform. Both of these mechanisms are seeing increased use across the ecosystem; for example, invokedynamic
has been used to implement a new form of string concatenation in Java 9 and above.
Understanding these features gives you key insight into the innermost workings of the platform and the modern frameworks upon which Java applications rely.
Lambda Expressions
One issue with anonymous classes is that if the implementation of your anonymous class is very simple, such as an interface that contains only one method, then the syntax of anonymous classes may seem unwieldy and unclear. In these cases, you're usually trying to pass functionality as an argument to another method, such as what action should be taken when someone clicks a button. Lambda expressions enable you to do this, to treat functionality as method argument, or code as data.
The previous section, Anonymous Classes, shows you how to implement a base class without giving it a name. Although this is often more concise than a named class, for classes with only one method, even an anonymous class seems a bit excessive and cumbersome. Lambda expressions let you express instances of single-method classes more compactly.
This section covers the following topics:
Ideal Use Case for Lambda Expressions
Approach 1: Create Methods That Search for Members That Match One Characteristic
Approach 2: Create More Generalized Search Methods
Approach 3: Specify Search Criteria Code in a Local Class
Approach 4: Specify Search Criteria Code in an Anonymous Class
Approach 5: Specify Search Criteria Code with a Lambda Expression
Approach 6: Use Standard Functional Interfaces with Lambda Expressions
Approach 7: Use Lambda Expressions Throughout Your Application
Approach 8: Use Generics More Extensively
Approach 9: Use Aggregate Operations That Accept Lambda Expressions as Parameters
Lambda Expressions in GUI Applications
Syntax of Lambda Expressions
Accessing Local Variables of the Enclosing Scope
Target Typing
Target Types and Method Arguments
Serialization
Ideal Use Case for Lambda Expressions
Suppose that you are creating a social networking application. You want to create a feature that enables an administrator to perform any kind of action, such as sending a message, on members of the social networking application that satisfy certain criteria. The following table describes this use case in detail:
Field | Description |
---|---|
Name | Perform action on selected members |
Primary Actor | Administrator |
Preconditions | Administrator is logged in to the system. |
Postconditions | Action is performed only on members that fit the specified criteria. |
Main Success Scenario |
|
Extensions | 1a. Administrator has an option to preview those members who match the specified criteria before he or she specifies the action to be performed or before selecting the Submit button. |
Frequency of Occurrence | Many times during the day. |
Suppose that members of this social networking application are represented by the following Person
class:
public class Person {
public enum Sex {
MALE, FEMALE
}
String name;
LocalDate birthday;
Sex gender;
String emailAddress;
public int getAge() {
// ...
}
public void printPerson() {
// ...
}
}
Suppose that the members of your social networking application are stored in a List<Person>
instance.
This section begins with a naive approach to this use case. It improves upon this approach with local and anonymous classes, and then finishes with an efficient and concise approach using lambda expressions. Find the code excerpts described in this section in the example RosterTest
.
Approach 1: Create Methods That Search for Members That Match One Characteristic
One simplistic approach is to create several methods; each method searches for members that match one characteristic, such as gender or age. The following method prints members that are older than a specified age:
public static void printPersonsOlderThan(List<Person> roster, int age) {
for (Person p : roster) {
if (p.getAge() >= age) {
p.printPerson();
}
}
}
Note: A List
is an ordered Collection
. A collection is an object that groups multiple elements into a single unit. Collections are used to store, retrieve, manipulate, and communicate aggregate data. For more information about collections, see the Collections trail.
This approach can potentially make your application brittle, which is the likelihood of an application not working because of the introduction of updates (such as newer data types). Suppose that you upgrade your application and change the structure of the Person
class such that it contains different member variables; perhaps the class records and measures ages with a different data type or algorithm. You would have to rewrite a lot of your API to accommodate this change. In addition, this approach is unnecessarily restrictive; what if you wanted to print members younger than a certain age, for example?
Approach 2: Create More Generalized Search Methods
The following method is more generic than printPersonsOlderThan
; it prints members within a specified range of ages:
public static void printPersonsWithinAgeRange(
List<Person> roster, int low, int high) {
for (Person p : roster) {
if (low <= p.getAge() && p.getAge() < high) {
p.printPerson();
}
}
}
What if you want to print members of a specified sex, or a combination of a specified gender and age range? What if you decide to change the Person
class and add other attributes such as relationship status or geographical location? Although this method is more generic than printPersonsOlderThan
, trying to create a separate method for each possible search query can still lead to brittle code. You can instead separate the code that specifies the criteria for which you want to search in a different class.
Approach 3: Specify Search Criteria Code in a Local Class
The following method prints members that match search criteria that you specify:
public static void printPersons(
List<Person> roster, CheckPerson tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}
This method checks each Person
instance contained in the List
parameter roster
whether it satisfies the search criteria specified in the CheckPerson
parameter tester
by invoking the method tester.test
. If the method tester.test
returns a true
value, then the method printPersons
is invoked on the Person
instance.
To specify the search criteria, you implement the CheckPerson
interface:
interface CheckPerson {
boolean test(Person p);
}
The following class implements the CheckPerson
interface by specifying an implementation for the method test
. This method filters members that are eligible for Selective Service in the United States: it returns a true
value if its Person
parameter is male and between the ages of 18 and 25:
class CheckPersonEligibleForSelectiveService implements CheckPerson {
public boolean test(Person p) {
return p.gender == Person.Sex.MALE &&
p.getAge() >= 18 &&
p.getAge() <= 25;
}
}
To use this class, you create a new instance of it and invoke the printPersons
method:
printPersons(
roster, new CheckPersonEligibleForSelectiveService());
Although this approach is less brittle—you don't have to rewrite methods if you change the structure of the Person
—you still have additional code: a new interface and a local class for each search you plan to perform in your application. Because CheckPersonEligibleForSelectiveService
implements an interface, you can use an anonymous class instead of a local class and bypass the need to declare a new class for each search.
Approach 4: Specify Search Criteria Code in an Anonymous Class
One of the arguments of the following invocation of the method printPersons
is an anonymous class that filters members that are eligible for Selective Service in the United States: those who are male and between the ages of 18 and 25:
printPersons(
roster,
new CheckPerson() {
public boolean test(Person p) {
return p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25;
}
}
);
This approach reduces the amount of code required because you don't have to create a new class for each search that you want to perform. However, the syntax of anonymous classes is bulky considering that the CheckPerson
interface contains only one method. In this case, you can use a lambda expression instead of an anonymous class, as described in the next section.
Approach 5: Specify Search Criteria Code with a Lambda Expression
The CheckPerson
interface is a functional interface. A functional interface is any interface that contains only one abstract method. (A functional interface may contain one or more default methods or static methods.) Because a functional interface contains only one abstract method, you can omit the name of that method when you implement it. To do this, instead of using an anonymous class expression, you use a lambda expression, which is highlighted in the following method invocation:
printPersons(
roster,
(Person p) -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
);
See Syntax of Lambda Expressions for information about how to define lambda expressions.
You can use a standard functional interface in place of the interface CheckPerson
, which reduces even further the amount of code required.
Approach 6: Use Standard Functional Interfaces with Lambda Expressions
Reconsider the CheckPerson
interface:
interface CheckPerson {
boolean test(Person p);
}
This is a very simple interface. It's a functional interface because it contains only one abstract method. This method takes one parameter and returns a boolean
value. The method is so simple that it might not be worth it to define one in your application. Consequently, the JDK defines several standard functional interfaces, which you can find in the package java.util.function
.
For example, you can use the Predicate<T>
interface in place of CheckPerson
. This interface contains the method boolean test(T t)
:
interface Predicate<T> {
boolean test(T t);
}
The interface Predicate<T>
is an example of a generic interface. (For more information about generics, see the Generics (Updated) lesson.) Generic types (such as generic interfaces) specify one or more type parameters within angle brackets (<>
). This interface contains only one type parameter, T
. When you declare or instantiate a generic type with actual type arguments, you have a parameterized type. For example, the parameterized type Predicate<Person>
is the following:
interface Predicate<Person> {
boolean test(Person t);
}
This parameterized type contains a method that has the same return type and parameters as CheckPerson.boolean test(Person p)
. Consequently, you can use Predicate<T>
in place of CheckPerson
as the following method demonstrates:
public static void printPersonsWithPredicate(
List<Person> roster, Predicate<Person> tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}
As a result, the following method invocation is the same as when you invoked printPersons
in Approach 3: Specify Search Criteria Code in a Local Class to obtain members who are eligible for Selective Service:
printPersonsWithPredicate(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
);
This is not the only possible place in this method to use a lambda expression. The following approach suggests other ways to use lambda expressions.
Approach 7: Use Lambda Expressions Throughout Your Application
Reconsider the method printPersonsWithPredicate
to see where else you could use lambda expressions:
public static void printPersonsWithPredicate(
List<Person> roster, Predicate<Person> tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}
This method checks each Person
instance contained in the List
parameter roster
whether it satisfies the criteria specified in the Predicate
parameter tester
. If the Person
instance does satisfy the criteria specified by tester
, the method printPerson
is invoked on the Person
instance.
Instead of invoking the method printPerson
, you can specify a different action to perform on those Person
instances that satisfy the criteria specified by tester
. You can specify this action with a lambda expression. Suppose you want a lambda expression similar to printPerson
, one that takes one argument (an object of type Person
) and returns void. Remember, to use a lambda expression, you need to implement a functional interface. In this case, you need a functional interface that contains an abstract method that can take one argument of type Person
and returns void. The Consumer<T>
interface contains the method void accept(T t)
, which has these characteristics. The following method replaces the invocation p.printPerson()
with an instance of Consumer<Person>
that invokes the method accept
:
public static void processPersons(
List<Person> roster,
Predicate<Person> tester,
Consumer<Person> block) {
for (Person p : roster) {
if (tester.test(p)) {
block.accept(p);
}
}
}
As a result, the following method invocation is the same as when you invoked printPersons
in Approach 3: Specify Search Criteria Code in a Local Class to obtain members who are eligible for Selective Service. The lambda expression used to print members is highlighted:
processPersons(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p -> p.printPerson()
);
What if you want to do more with your members' profiles than printing them out. Suppose that you want to validate the members' profiles or retrieve their contact information? In this case, you need a functional interface that contains an abstract method that returns a value. The Function<T,R>
interface contains the method R apply(T t)
. The following method retrieves the data specified by the parameter mapper
, and then performs an action on it specified by the parameter block
:
public static void processPersonsWithFunction(
List<Person> roster,
Predicate<Person> tester,
Function<Person, String> mapper,
Consumer<String> block) {
for (Person p : roster) {
if (tester.test(p)) {
String data = mapper.apply(p);
block.accept(data);
}
}
}
The following method retrieves the email address from each member contained in roster
who is eligible for Selective Service and then prints it:
processPersonsWithFunction(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p -> p.getEmailAddress(),
email -> System.out.println(email)
);
Approach 8: Use Generics More Extensively
Reconsider the method processPersonsWithFunction
. The following is a generic version of it that accepts, as a parameter, a collection that contains elements of any data type:
public static <X, Y> void processElements(
Iterable<X> source,
Predicate<X> tester,
Function <X, Y> mapper,
Consumer<Y> block) {
for (X p : source) {
if (tester.test(p)) {
Y data = mapper.apply(p);
block.accept(data);
}
}
}
To print the e-mail address of members who are eligible for Selective Service, invoke the processElements
method as follows:
processElements(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p -> p.getEmailAddress(),
email -> System.out.println(email)
);
This method invocation performs the following actions:
Obtains a source of objects from the collection
source
. In this example, it obtains a source ofPerson
objects from the collectionroster
. Notice that the collectionroster
, which is a collection of typeList
, is also an object of typeIterable
.Filters objects that match the
Predicate
objecttester
. In this example, thePredicate
object is a lambda expression that specifies which members would be eligible for Selective Service.Maps each filtered object to a value as specified by the
Function
objectmapper
. In this example, theFunction
object is a lambda expression that returns the e-mail address of a member.Performs an action on each mapped object as specified by the
Consumer
objectblock
. In this example, theConsumer
object is a lambda expression that prints a string, which is the e-mail address returned by theFunction
object.
You can replace each of these actions with an aggregate operation.
Approach 9: Use Aggregate Operations That Accept Lambda Expressions as Parameters
The following example uses aggregate operations to print the e-mail addresses of those members contained in the collection roster
who are eligible for Selective Service:
roster
.stream()
.filter(
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25)
.map(p -> p.getEmailAddress())
.forEach(email -> System.out.println(email));
The following table maps each of the operations the method processElements
performs with the corresponding aggregate operation:
processElements Action | Aggregate Operation |
---|---|
Obtain a source of objects | Stream<E> stream() |
Filter objects that match a Predicate object | Stream<T> filter(Predicate<? super T> predicate) |
Map objects to another value as specified by a Function object | <R> Stream<R> map(Function<? super T,? extends R> mapper) |
Perform an action as specified by a Consumer object | void forEach(Consumer<? super T> action) |
The operations filter
, map
, and forEach
are aggregate operations. Aggregate operations process elements from a stream, not directly from a collection (which is the reason why the first method invoked in this example is stream
). A stream is a sequence of elements. Unlike a collection, it is not a data structure that stores elements. Instead, a stream carries values from a source, such as collection, through a pipeline. A pipeline is a sequence of stream operations, which in this example is filter
- map
-forEach
. In addition, aggregate operations typically accept lambda expressions as parameters, enabling you to customize how they behave.
For a more thorough discussion of aggregate operations, see the Aggregate Operations lesson.
Lambda Expressions in GUI Applications
To process events in a graphical user interface (GUI) application, such as keyboard actions, mouse actions, and scroll actions, you typically create event handlers, which usually involves implementing a particular interface. Often, event handler interfaces are functional interfaces; they tend to have only one method.
In the JavaFX example HelloWorld.java
(discussed in the previous section Anonymous Classes), you can replace the highlighted anonymous class with a lambda expression in this statement:
btn.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
System.out.println("Hello World!");
}
});
The method invocation btn.setOnAction
specifies what happens when you select the button represented by the btn
object. This method requires an object of type EventHandler<ActionEvent>
. The EventHandler<ActionEvent>
interface contains only one method, void handle(T event)
. This interface is a functional interface, so you could use the following highlighted lambda expression to replace it:
btn.setOnAction(
event以上是关于Java Lambda 表达式与 JVM 中的 Invoke Dynamic 简介的主要内容,如果未能解决你的问题,请参考以下文章