Java Lambda 表达式与 JVM 中的 Invoke Dynamic 简介

本文深入探讨了Java 7引入的InvokeDynamic指令,解释了它是如何增强JVM对动态语言的支持,尤其是在处理Lambda表达式时的作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

28ed4b1f90ca467ba64e5941c285f3d2.png

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 操作码的第一次,它调用被称为引导方法来初始化调用过程的特殊方法:

09dc6062b2dd9c4bc68be1cdbfc07ab0.png

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): ConstantCallSiteMutableCallSite, 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 occurs

  • String: The name mentioned in the NameAndType

  • MethodType: The resolved type descriptor of the NameAndType

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:

FieldDescription
NamePerform action on selected members
Primary ActorAdministrator
PreconditionsAdministrator is logged in to the system.
PostconditionsAction is performed only on members that fit the specified criteria.
Main Success Scenario
  1. Administrator specifies criteria of members on which to perform a certain action.

  2. Administrator specifies an action to perform on those selected members.

  3. Administrator selects the Submit button.

  4. The system finds all members that match the specified criteria.

  5. The system performs the specified action on all matching members.

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 OccurrenceMany 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:

  1. Obtains a source of objects from the collection source. In this example, it obtains a source of Person objects from the collection roster. Notice that the collection roster, which is a collection of type List, is also an object of type Iterable.

  2. Filters objects that match the Predicate object tester. In this example, the Predicate object is a lambda expression that specifies which members would be eligible for Selective Service.

  3. Maps each filtered object to a value as specified by the Function object mapper. In this example, the Function object is a lambda expression that returns the e-mail address of a member.

  4. Performs an action on each mapped object as specified by the Consumer object block. In this example, the Consumer object is a lambda expression that prints a string, which is the e-mail address returned by the Function 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 ActionAggregate Operation
Obtain a source of objectsStream<E> stream()
Filter objects that match a Predicate objectStream<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 objectvoid forEach(Consumer<? super T> action)

The operations filtermap, 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 filtermap-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 -> System.out.println("Hello World!")
        );

Syntax of Lambda Expressions

A lambda expression consists of the following:

  • A comma-separated list of formal parameters enclosed in parentheses. The CheckPerson.test method contains one parameter, p, which represents an instance of the Person class.

    Note: You can omit the data type of the parameters in a lambda expression. In addition, you can omit the parentheses if there is only one parameter. For example, the following lambda expression is also valid:

    p -> p.getGender() == Person.Sex.MALE 
        && p.getAge() >= 18
        && p.getAge() <= 25
  • The arrow token, ->

  • A body, which consists of a single expression or a statement block. This example uses the following expression:

    p.getGender() == Person.Sex.MALE 
        && p.getAge() >= 18
        && p.getAge() <= 25

    If you specify a single expression, then the Java runtime evaluates the expression and then returns its value. Alternatively, you can use a return statement:

    p -> {
        return p.getGender() == Person.Sex.MALE
            && p.getAge() >= 18
            && p.getAge() <= 25;
    }

    A return statement is not an expression; in a lambda expression, you must enclose statements in braces ({}). However, you do not have to enclose a void method invocation in braces. For example, the following is a valid lambda expression:

    email -> System.out.println(email)

Note that a lambda expression looks a lot like a method declaration; you can consider lambda expressions as anonymous methods—methods without a name.

The following example, Calculator, is an example of lambda expressions that take more than one formal parameter:

public class Calculator {
  
    interface IntegerMath {
        int operation(int a, int b);   
    }
  
    public int operateBinary(int a, int b, IntegerMath op) {
        return op.operation(a, b);
    }
 
    public static void main(String... args) {
    
        Calculator myApp = new Calculator();
        IntegerMath addition = (a, b) -> a + b;
        IntegerMath subtraction = (a, b) -> a - b;
        System.out.println("40 + 2 = " +
            myApp.operateBinary(40, 2, addition));
        System.out.println("20 - 10 = " +
            myApp.operateBinary(20, 10, subtraction));    
    }
}

The method operateBinary performs a mathematical operation on two integer operands. The operation itself is specified by an instance of IntegerMath. The example defines two operations with lambda expressions, addition and subtraction. The example prints the following:

40 + 2 = 42
20 - 10 = 10

Accessing Local Variables of the Enclosing Scope

Like local and anonymous classes, lambda expressions can capture variables; they have the same access to local variables of the enclosing scope. However, unlike local and anonymous classes, lambda expressions do not have any shadowing issues (see Shadowing for more information). Lambda expressions are lexically scoped. This means that they do not inherit any names from a supertype or introduce a new level of scoping. Declarations in a lambda expression are interpreted just as they are in the enclosing environment. The following example, LambdaScopeTest, demonstrates this:

import java.util.function.Consumer;
 
public class LambdaScopeTest {
 
    public int x = 0;
 
    class FirstLevel {
 
        public int x = 1;
        
        void methodInFirstLevel(int x) {

            int z = 2;
             
            Consumer<Integer> myConsumer = (y) -> 
            {
                // The following statement causes the compiler to generate
                // the error "Local variable z defined in an enclosing scope
                // must be final or effectively final" 
                //
                // z = 99;
                
                System.out.println("x = " + x); 
                System.out.println("y = " + y);
                System.out.println("z = " + z);
                System.out.println("this.x = " + this.x);
                System.out.println("LambdaScopeTest.this.x = " +
                    LambdaScopeTest.this.x);
            };
 
            myConsumer.accept(x);
 
        }
    }
 
    public static void main(String... args) {
        LambdaScopeTest st = new LambdaScopeTest();
        LambdaScopeTest.FirstLevel fl = st.new FirstLevel();
        fl.methodInFirstLevel(23);
    }
}

This example generates the following output:

x = 23
y = 23
z = 2
this.x = 1
LambdaScopeTest.this.x = 0

If you substitute the parameter x in place of y in the declaration of the lambda expression myConsumer, then the compiler generates an error:

Consumer<Integer> myConsumer = (x) -> {
    // ...
}

The compiler generates the error "Lambda expression's parameter x cannot redeclare another local variable defined in an enclosing scope" because the lambda expression does not introduce a new level of scoping. Consequently, you can directly access fields, methods, and local variables of the enclosing scope. For example, the lambda expression directly accesses the parameter x of the method methodInFirstLevel. To access variables in the enclosing class, use the keyword this. In this example, this.x refers to the member variable FirstLevel.x.

However, like local and anonymous classes, a lambda expression can only access local variables and parameters of the enclosing block that are final or effectively final. In this example, the variable z is effectively final; its value is never changed after it's initialized. However, suppose that you add the following assignment statement in the the lambda expression myConsumer:

Consumer<Integer> myConsumer = (y) -> {
    z = 99;
    // ...
}

Because of this assignment statement, the variable z is not effectively final anymore. As a result, the Java compiler generates an error message similar to "Local variable z defined in an enclosing scope must be final or effectively final".

Target Typing

How do you determine the type of a lambda expression? Recall the lambda expression that selected members who are male and between the ages 18 and 25 years:

p -> p.getGender() == Person.Sex.MALE
    && p.getAge() >= 18
    && p.getAge() <= 25

This lambda expression was used in the following two methods:

  • public static void printPersons(List<Person> roster, CheckPerson tester) in Approach 3: Specify Search Criteria Code in a Local Class

  • public void printPersonsWithPredicate(List<Person> roster, Predicate<Person> tester) in Approach 6: Use Standard Functional Interfaces with Lambda Expressions

When the Java runtime invokes the method printPersons, it's expecting a data type of CheckPerson, so the lambda expression is of this type. However, when the Java runtime invokes the method printPersonsWithPredicate, it's expecting a data type of Predicate<Person>, so the lambda expression is of this type. The data type that these methods expect is called the target type. To determine the type of a lambda expression, the Java compiler uses the target type of the context or situation in which the lambda expression was found. It follows that you can only use lambda expressions in situations in which the Java compiler can determine a target type:

  • Variable declarations

  • Assignments

  • Return statements

  • Array initializers

  • Method or constructor arguments

  • Lambda expression bodies

  • Conditional expressions, ?:

  • Cast expressions

Target Types and Method Arguments

For method arguments, the Java compiler determines the target type with two other language features: overload resolution and type argument inference.

Consider the following two functional interfaces ( java.lang.Runnable and java.util.concurrent.Callable<V>):

public interface Runnable {
    void run();
}

public interface Callable<V> {
    V call();
}

The method Runnable.run does not return a value, whereas Callable<V>.call does.

Suppose that you have overloaded the method invoke as follows (see Defining Methods for more information about overloading methods):

void invoke(Runnable r) {
    r.run();
}

<T> T invoke(Callable<T> c) {
    return c.call();
}

Which method will be invoked in the following statement?

String s = invoke(() -> "done");

The method invoke(Callable<T>) will be invoked because that method returns a value; the method invoke(Runnable) does not. In this case, the type of the lambda expression () -> "done" is Callable<T>.

Serialization

You can serialize a lambda expression if its target type and its captured arguments are serializable. However, like inner classes, the serialization of lambda expressions is strongly discouraged.

<think>嗯,用户想了解Java基础知识中的invoke”,也就是调用方法。首先,我需要确认用户指的是哪个方面的invokeJava中有几种不同的调用方式,比如方法调用、反射中的Method.invoke(),或者动态代理、Lambda表达式相关的invokedynamic指令。可能需要逐一解释这些内容,帮助用户全面理解。 首先,最基础的是普通的方法调用,比如实例方法、静态方法、构造方法。这部分应该先讲清楚,包括如何通过对象调用实例方法,用类名调用静态方法,以及使用new关键字调用构造方法。然后可以举一些例子,比如System.out.println或者Math.max这样的静态方法。 接下来是反射中的Method.invoke(),这是高级特性,允许在运行时动态调用方法。需要说明反射的作用,如何获取Method对象,然后使用invoke()来执行。这里要注意到访问权限的问题,比如私有方法需要setAccessible(true),还要提到异常处理,因为反射调用可能会抛出异常。 然后是动态代理,这里涉及到InvocationHandler接口和invoke()方法。动态代理常用于AOP、日志、事务管理等,需要解释代理对象如何拦截方法调用,并通过InvocationHandler的invoke方法来处理。可以举例说明如何创建代理对象,并实现额外的逻辑。 还有Java 7引入的invokedynamic指令,这是为了支持动态类型语言,后来被用于Lambda表达式和方法引用。这部分可能比较复杂,用户如果刚接触可能不太熟悉,可以简单说明其用途,比如Lambda表达式在底层是如何通过invokedynamic实现的,而不需要深入字节码细节。 另外,用户可能对方法调用的底层机制感兴趣,比如栈帧、局部变量表、操作数栈等,但考虑到是基础知识,可能暂时不需要深入JVM细节。不过如果用户有进一步学习的兴趣,可以提及这些概念,供他们后续探索。 需要确保回答结构清晰,分点讲解,每个部分用标题分隔,举例子帮助理解。同时注意使用中文,避免术语混淆,比如正确翻译术语如“反射”、“动态代理”等。还要检查LaTeX格式是否正确,行内公式用$...$,独立公式用$$...$$,虽然当前问题可能涉及代码较多,但如果有数学表达式的话要符合格式要求。 可能用户在学习Java的过程中遇到了方法调用的不同形式,或者在反射、动态代理方面有疑问。需要确保回答覆盖主要的invoke相关知识点,帮助他们建立系统的理解。同时,提醒异常处理和安全考虑,比如反射的性能开销和权限问题,这是实际开发中需要注意的地方。 最后,总结不同场景下的invoke应用,强调根据需求选择合适的方式,比如普通调用在编码时已知方法,反射用于运行时动态调用,动态代理用于增强功能,invokedynamic支持灵活的语言特性。这样用户能根据不同的情况选择合适的调用方法。</think>以下是对Java中“invoke”(方法调用)基础知识的系统讲解,按应用场景分层说明: --- ### 一、基础方法调用 Java中所有方法执行都通过`invoke`实现,分为以下类型: 1. **实例方法调用** ```java Object obj = new Object(); obj.toString(); // 通过对象实例调用方法 ``` - 需先创建对象实例 - 隐含传递`this`参数 2. **静态方法调用** ```java Math.max(1, 2); // 通过类名直接调用 ``` - 不需要对象实例 - 属于类级别方法 3. **构造方法调用** ```java String s = new String(); // 通过new关键字调用 ``` - 隐式返回新对象 - 可重载不同参数版本 --- ### 二、反射调用(Reflection API) 通过`java.lang.reflect.Method`实现动态调用: ```java Class<?> clazz = Class.forName("com.example.MyClass"); Method method = clazz.getMethod("methodName", String.class); Object result = method.invoke(clazz.newInstance(), "参数"); // 关键调用 ``` - **特点** - 运行时动态获取方法信息 - 可突破访问权限限制(需调用`method.setAccessible(true)`) - 性能较低(比直接调用慢约10倍) - **异常处理** 需捕获: ```java try { method.invoke(...); } catch (IllegalAccessException | InvocationTargetException e) { e.printStackTrace(); } ``` --- ### 三、动态代理(Dynamic Proxy) 通过`java.lang.reflect.InvocationHandler`实现方法拦截: ```java public class MyHandler implements InvocationHandler { private Object target; @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("方法调用前"); Object result = method.invoke(target, args); // 实际方法调用 System.out.println("方法调用后"); return result; } } ``` - **应用场景** - AOP(如日志、事务) - RPC框架调用拦截 - 接口方法统一增强 --- ### 四、invokedynamic指令(Java 7+) 底层字节码指令,支持动态语言特性: ```java // Lambda表达式底层实现 Runnable r = () -> System.out.println("Hello"); // 等效字节码使用invokedynamic ``` - **特性** - 延迟绑定:运行时确定方法实现 - 提升动态语言(如Groovy)兼容性 - Lambda表达式和方法引用的实现基础 --- ### 五、方法调用底层机制 1. **栈帧结构** - 局部变量表(存储参数和局部变量) - 操作数栈(计算中间结果) - 动态链接(指向方法引用) 2. **分派机制** - 静态分派(重载) - 动态分派(重写) --- ### 总结对比 | 调用方式 | 使用场景 | 性能 | 灵活性 | |---------------|---------------------------------|-------|-------| | 直接调用 | 编译期已知方法签名 | 最高 | 低 | | 反射调用 | 运行时动态调用(如框架) | 较低 | 高 | | 动态代理 | 接口方法增强 | 中等 | 高 | | invokedynamic | 动态语言支持/Lambda表达式 | JIT优化 | 最高 | 实际开发中优先选择直接调用,反射和动态代理应谨慎使用。理解这些机制有助于掌握Java核心运行原理。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值