Java lambda 实现逻辑,基于 openjdk lambda 文档

发布时间:2023年12月17日

参考
https://cr.openjdk.org/~briangoetz/lambda/lambda-translation.html

主要记录 Java 中如何将 Lambda 翻译为字节码。结合示例描述这个过程

函数式接口

函数式接口是Java中Lambda表达式的核心组成部分。

Lambda表达式只能出现在将其分配给函数式接口类型的变量的地方。例如:
变量:

javaCopy code
Runnable r = () -> { System.out.println("hello"); };

函数参数:

javaCopy code
Collections.sort(strings, (String a, String b) -> -(a.compareTo(b)));

函数式接口是具有一个非Object方法的接口,比如Runnable、Comparator等(多年来,Java库一直在使用这些接口来表示回调函数)。例如:
消费者接口只有 accept 一个抽象方法
image.png
Compare 也只有 compare 一个非Object方法的接口
image.png

lambda 转换

  1. 当编译器遇到Lambda表达式时,首先将Lambda体降低(解糖)为一个方法,其参数列表和返回类型与Lambda表达式相匹配,可能还包括一些额外的参数(用于从词法范围中捕获的值,如果有的话)。
  2. 在Lambda表达式将被捕获的地方,它生成一个invokedynamic调用点,当调用时,返回Lambda正在转换的函数接口的实例。对于给定的Lambda,此调用点被称为Lambda工厂。Lambda工厂的动态参数是从词法范围中捕获的值。Lambda工厂的引导方法是Java语言运行库中的一个标准方法,称为Lambda元工厂。引导方法的静态引导参数捕获了在编译时已知的与Lambda有关的信息(将要转换的函数接口,解糖Lambda体的方法句柄,有关SAM类型是否可序列化的信息等)。

例如:
原代码:

public class GenericTypes {

    public static void main(String[] args) {
        List<Integer> collect = new ArrayList<>(List.of(1,2,3,4));
        Comparator<Integer> s = (s1,s2) -> {
            if(s1 == s2) return 0;
            else if(s1 > s2) return -1;
            else return 1;
        };

        collect.sort(s);
        for(Integer integer:collect)
            System.out.println(String.valueOf(integer));
    }
}

脱糖代码:
使用 CFR 0.152. 反编译字节码可以看到 lambda 脱糖后的代码
Java 语言中,javac命令可以将后缀名为.java的源文件编译为后缀名为.class的可以运行于 Java 虚拟机的字节码。如果你去看com.sun.tools.javac.main.JavaCompiler的源码,你会发现在compile()中有一个步骤就是调用desugar(),这个方法就是负责解语法糖的实现的。

java -jar src/main/resources/cfr-0.152.jar target/classes/org/example/GenericTypes.class --decodelambdas false
/*
 * Decompiled with CFR 0.152.
 */
public class GenericTypes {
    public static void main(String[] args) {
        List collect = IntStream.range(0, 20).boxed().collect(Collectors.toList());
        Comparator s = (Comparator)LambdaMetafactory.metafactory(null, null, null, (Ljava/lang/Object;Ljava/lang/Object;)I, lambda$main$0(java.lang.Integer java.lang.Integer ), (Ljava/lang/Integer;Ljava/lang/Integer;)I)();        
        ArrayList arrayList = new ArrayList();
        collect.sort(s);
        for (Integer integer : collect) {
            System.out.println(String.valueOf(integer));
        }
        PriorityQueue priorityQueue = new PriorityQueue(collect);
        Iterator iterator = priorityQueue.iterator();
        while (iterator.hasNext()) {
            System.out.println(String.valueOf(iterator.next()));
            iterator.remove();
        }
    }

    private static /* synthetic */ int lambda$main$0(Integer s1, Integer s2) {
        if (s1 == s2) {
            return 0;
        }
        if (s1 > s2) {
            return -1;
        }
        return 1;
    }
}

lmbdaMetaFactory 调试代码

  1. MethodHandles.Lookup caller: caller 参数是一个 MethodHandles.Lookup 对象,它提供对调用者的访问权限,用于在指定的上下文中查找方法。通常,这是调用方的 Lookup 对象。
  2. String interfaceMethodName: interfaceMethodName 参数是一个字符串,表示接口中的方法名。Lambda 表达式将要实现的接口方法的名字。
  3. MethodType factoryType: factoryType 参数是一个 MethodType 对象,表示 Lambda 表达式工厂方法的类型。这是 Lambda 表达式创建时的方法类型。
  4. MethodType interfaceMethodType: interfaceMethodType 参数是一个 MethodType 对象,表示接口方法的类型。这是 Lambda 表达式要实现的接口方法的方法类型。
  5. MethodHandle implementation: implementation 参数是一个 MethodHandle 对象,表示 Lambda 表达式的具体实现。这是一个与接口方法具有相同签名的方法句柄。
  6. MethodType dynamicMethodType: dynamicMethodType 参数是一个 MethodType 对象,表示动态调用时 Lambda 表达式的方法类型。通常与 factoryType 不同,因为 Lambda 表达式的实例方法可能具有不同的方法类型。

image.png
image.png

解糖

在将Lambda表达式翻译成字节码的过程中,第一步是对Lambda体进行解糖,将其转化为一个方法。
1. 解糖的选择
在解糖过程中需要做一些选择:

  • 我们是解糖为静态方法还是实例方法?
  • 解糖后的方法应该放在哪个类中?
  • 解糖方法的可访问性应该是什么?
  • 解糖方法的名称应该是什么?
  • 如果需要在Lambda体签名和函数式接口方法签名之间进行适应(如装箱、拆箱、原始类型的拓宽或缩小转换,可变参数转换等),解糖方法应该遵循Lambda体的签名、函数式接口方法的签名,还是两者之间的某种情况?对于所需的适应,由谁负责?
  • 如果Lambda从封闭作用域中捕获参数,应该如何在解糖的方法签名中表示这些参数?(它们可以作为单独的参数添加到参数列表的开头或末尾,或者编译器可以将它们收集到一个单独的“frame”参数中。)

2. 方法引用的适配器问题
与解糖Lambda体的问题相关的是方法引用是否需要生成适配器或“桥接”方法。
编译器将为Lambda表达式推断出一个方法签名,包括参数类型、返回类型和可能抛出的异常;我们将其称为“自然签名”。Lambda表达式还有一个目标类型,这将是一个函数式接口;我们将Lambda描述符称为目标类型的擦除的方法签名的描述符。Lambda工厂返回的值,该工厂实现了函数式接口并捕获了Lambda的行为,被称为Lambda对象。
3. 偏好和例外情况
在所有其他条件相等的情况下,私有方法优于非私有方法,静态方法优于实例方法,最好将Lambda体解糖到Lambda表达式所在的最内层类中,签名应与Lambda体的签名匹配,额外的参数应该添加到参数列表的前面以捕获的值,最好根本不解糖方法引用。然而,有例外情况,我们可能需要偏离这个基线策略。
解糖Lambda表达式是字节码生成的一个关键步骤,影响着Lambda在Java中的实现和性能。通过解糖,我们能更好地理解Lambda背后的实现机制,以及在不同场景下作出的权衡和选择。

无状态Lambda表达式的解糖实例

无状态Lambda表达式是最简单的一种形式,它不从封闭作用域中捕获任何状态。例如:

class A {
    public void foo() {
        List<String> list = ...
        list.forEach( s -> { System.out.println(s); } );
    }
}

对于这种情况,Lambda的自然签名是**(String)V**,而forEach方法接受一个Block,其Lambda描述符为**(Object)V**。编译器将Lambda体解糖为一个自然签名的静态方法,并为解糖后的方法生成一个名称:

class A {
    public void foo() {
        List<String> list = ...
        list.forEach( [lambda for lambda$1 as Block] );
    }

    static void lambda$1(String s) {
        System.out.println(s);
    }
}
捕获不可变值的Lambda表达式的解糖实例

另一种Lambda表达式涉及捕获封闭作用域中的最终(或有效最终)局部变量和/或封闭实例的字段(我们将其视为对最终封闭this引用的捕获)。

class B {
    public void foo() {
        List<Person> list = ...
        final int bottom = ..., top = ...;
        list.removeIf( p -> (p.size >= bottom && p.size <= top) );
    }
}

在这里,Lambda捕获了封闭作用域中的最终局部变量bottomtop
解糖后方法的签名将是自然签名**(Person)Z**,并在参数列表的前面附加了一些额外的参数。这些额外的参数的表示方式由编译器决定,可以分别附加,装箱成一个帧类,装箱成一个数组等。最简单的方法是将它们分别附加:

class B {
    public void foo() {
        List<Person> list = ...
        final int bottom = ..., top = ...;
        list.removeIf( [ lambda for lambda$1 as Predicate capturing (bottom, top) ]);
    }

    static boolean lambda$1(int bottom, int top, Person p) {
        return (p.size >= bottom && p.size <= top);
    }

或者,捕获的值(bottomtop)可以被装箱成一个帧或数组;关键是在解糖的Lambda方法签名中,额外参数的类型与它们作为Lambda工厂的(动态)参数的类型一致。由于编译器同时控制这两者,它在捕获参数如何打包方面具有一定的灵活性。

Lambda Metafactory

在Java中,Lambda表达式的实现涉及到Lambda Metafactory,这是一个通过invokedynamic调用站实现的机制。Lambda Metafactory的静态参数描述了Lambda体和Lambda描述符的特征,而动态参数(如果有的话)则是捕获的值。当调用时,这个调用站返回一个Lambda对象,该对象与相应的Lambda体和描述符绑定到捕获的值上。这个调用站的引导方法是一个特定的平台方法,称为Lambda Metafactory。可以为所有Lambda形式使用一个通用的Metafactory,或者为常见情况制定专门的版本。
1. Lambda Metafactory的基本结构
Lambda捕获将通过invokedynamic调用站来实现,其静态参数如下:

metaFactory(MethodHandles.Lookup caller, // 由VM提供
            String invokedName,          // 由VM提供
            MethodType invokedType,      // 由VM提供
            MethodHandle descriptor,     // Lambda描述符
            MethodHandle impl)           // Lambda体

前三个参数(caller、invokedName、invokedType)是由VM在调用站链接时自动堆叠的。

  • descriptor参数标识将Lambda转换为的函数式接口方法。通过反射API获取方法句柄,Lambda Metafactory可以获取函数式接口类的名称以及其主要方法的名称和方法签名。
  • impl参数标识Lambda方法,可以是解糖的Lambda体或方法引用中指定的方法。

2. 方法签名的差异与适应
函数式接口方法和实现方法之间可能存在一些差异。实现方法可能具有与捕获参数对应的额外参数。其他参数也可能不完全匹配;在适应中允许一些差异(子类型,装箱),如适应中所述。
Lambda Metafactory机制允许Lambda表达式的灵活实现,通过动态创建Lambda对象并将其绑定到实际的Lambda体和捕获的值。这种机制的设计使得Lambda表达式在Java中得以优雅而高效地实现,为函数式编程风格提供了强大的支持。通过深入理解Lambda Metafactory,我们能够更好地理解Java编程语言中Lambda表达式的底层工作原理。

无状态Lambda表达式的实例

在示例A中,我们展示了一个简单的Lambda表达式的转换:

class A {
    public void foo() {
        List<String> list = ...
        list.forEach(indy((MH(metaFactory), MH(invokeVirtual Block.apply),
                          MH(invokeStatic A.lambda$1)( )));
    }

    private static void lambda$1(String s) {
        System.out.println(s);
    }
}

这里,indy表示一个与动态调用相关的操作,而MH则代表MethodHandle。Lambda表达式被转换为一个静态方法lambda$1,并通过MethodHandle进行调用。

捕获不可变值的Lambda表达式的实例

在示例B中,Lambda表达式引用了外部作用域的bottomtop变量:

javaCopy code
class B {
    public void foo() {
        List<Person> list = ...
        final int bottom = ..., top = ...;
        list.removeIf(indy((MH(metaFactory), MH(invokeVirtual Predicate.apply),
                           MH(invokeStatic B.lambda$1))( bottom, top ))));
    }

    private static boolean lambda$1(int bottom, int top, Person p) {
        return (p.size >= bottom && p.size <= top;
    }
}

这种情况下,Lambda捕获使得Lambda表达式能够访问并操作外部作用域的变量,从而增强了Lambda的灵活性。

lambda 是否转换为静态方法

  1. 非捕获实例的Lambda(Non-instance-capturing lambdas):这些Lambda不使用外围对象实例的任何方式,即它们不引用this、super或外围实例的成员。这样的Lambda会被翻译成私有的静态方法(private, static methods)。
  2. 捕获实例的Lambda(Instance-capturing lambdas):这些Lambda使用this、super或者捕获外围实例的成员。这样的Lambda会被翻译成私有的实例方法(private instance methods)。例如:
list.forEach(e -> System.out.println(e.getSize()));

翻译为:

private void lambda$1(Element e) {
System.out.println(e.getSize());
}

总体来说,捕获实例的Lambda被翻译成私有实例方法,而非捕获实例的Lambda被翻译成私有静态方法。这样的翻译简化了Lambda的处理,因为Lambda体中的名称在翻译后的方法中与Lambda体中的名称相同,这也与可用的实现技术(绑定方法句柄)相吻合。

Java 11 测试

在Java 11 中发现全部使用静态方法实现,实现策略改变了
代码

public class GenericTypes {

    private int size;
    public int getSize() {
        return size;
    }

    public static void test() {
        System.out.printf("test method");
    }

    public static void main(String[] args) {
        GenericTypes genericTypes = new GenericTypes();

        int minSize = 3;
        Consumer<String> consumer = e-> {
            System.out.printf(e);
        } ;
        Predicate<GenericTypes> predicate = e -> {
            return e.getSize() < minSize;
        };

        Method[] declaredMethods = genericTypes.getClass().getDeclaredMethods();
        for(Method method : declaredMethods) {
            System.out.print("方法名: " + method.getName() + ", 是否为静态方法: ");
            System.out.println(String.valueOf(Modifier.isStatic(method.getModifiers())));
        }
    }
}

运行结果

方法名: main, 是否为静态方法: true
方法名: test, 是否为静态方法: true
方法名: getSize, 是否为静态方法: false
方法名: lambda$main$1, 是否为静态方法: true
方法名: lambda$main$0, 是否为静态方法: true

任意参数的方法

在处理可变参数(varargs)方法的方法引用时,如果转换为不是可变参数方法的函数式接口,编译器必须生成桥接方法并捕获桥接方法的方法句柄,而不是目标方法本身。该桥接方法必须处理所需的参数类型适应以及从可变参数到非可变参数的转换。以下是一个具体的例子:

interface IIS {
    void foo(Integer a1, Integer a2, String a3);
}

class Foo {
    static void m(Number a1, Object... rest) { /* 实现省略 */ }
}

class Bar {
    void bar() {
        IIS x = Foo::m;
    }
}

在上述例子中,编译器需要生成一个桥接方法,执行第一个参数类型的适应(从Number到Integer的转换),并将剩余的参数收集到一个Object数组中。生成的桥接方法如下:

class Bar {
    void bar() {
        IIS x = (a1, a2, a3) -> Foo.m((Integer) a1, a2, a3);
    }
}

这里,Lambda表达式中的参数类型适应和可变参数的转换都已经被处理。对应的桥接方法的实现类似于你提供的例子,但是Lambda表达式为我们提供了更简洁的语法。

lambda 为什么使用这个策略

There are a number of ways we might represent a lambda expression in bytecode, such as inner classes, method handles, dynamic proxies, and others. Each of these approaches has pros and cons. In selecting a strategy, there are two competing goals: maximizing flexibility for future optimization by not committing to a specific strategy, vs providing stability in the classfile representation. We can achieve both of these goals by using the invokedynamic feature from JSR 292 to separate the binary representation of lambda creation in the bytecode from the mechanics of evaluating the lambda expression at runtime. Instead of generating bytecode to create the object that implements the lambda expression (such as calling a constructor for an inner class), we describe a recipe for constructing the lambda, and delegate the actual construction to the language runtime. That recipe is encoded in the static and dynamic argument lists of an invokedynamic instruction.
The use of invokedynamic lets us defer the selection of a translation strategy until run time. The runtime implementation is free to select a strategy dynamically to evaluate the lambda expression. The runtime implementation choice is hidden behind a standardized (i.e., part of the platform specification) API for lambda construction, so that the static compiler can emit calls to this API, and JRE implementations can choose their preferred implementation strategy. The invokedynamic mechanics allow this to be done without the performance costs that this late binding approach might otherwise impose.

在Java中,表示Lambda表达式的字节码有多种方式,包括内部类、方法句柄、动态代理等。每种方法都有其优缺点。在选择策略时,存在两个相互竞争的目标:通过不承诺特定策略来最大化未来优化的灵活性,与在类文件表示中提供稳定性。通过使用JSR 292中的invokedynamic特性,我们可以实现这两个目标,将字节码中Lambda创建的二进制表示与在运行时评估Lambda表达式的机制分离开来。
1. 分离二进制表示与运行时机制
传统方法中,为了创建实现Lambda表达式的对象,会生成相应的字节码,比如调用内部类的构造函数。然而,invokedynamic机制改变了这一做法,我们不再直接生成用于创建对象的字节码,而是描述构建Lambda的方法,并将实际的构建过程委托给语言运行时。这个构建Lambda的方法被编码在invokedynamic指令的静态和动态参数列表中。
2. 推迟翻译策略选择
使用invokedynamic允许我们将翻译策略的选择推迟到运行时。运行时实现可以动态选择策略来评估Lambda表达式,而这个选择被隐藏在标准化的Lambda构建API背后。这个API是平台规范的一部分,静态编译器可以调用它,而JRE实现可以选择他们首选的实现策略。
3. 性能成本的优化
invokedynamic机制使得在运行时进行策略选择的方式变得高效,避免了晚期绑定方法可能带来的性能成本。通过隐藏实现细节,invokedynamic使得字节码的生成和运行时的Lambda表达式评估之间的交互更加灵活、高效。
在Java中,这一特性为Lambda表达式的使用提供了更大的灵活性,并通过invokedynamic机制的引入,成功平衡了未来优化的灵活性和类文件表示的稳定性。这使得Java编程在处理函数式编程范式时更加强大和高效。

lambda 序列化

Java中的Lambda表达式是一种强大的编程工具,但在序列化(Serialization)过程中,涉及到一些动态翻译策略,特别是需要能够在不同的实现之间切换,比如今天可能使用内部类,而明天可能切换到使用动态代理。为了实现这样的动态序列化策略,需要定义一种中性的序列化形式,通过readResolve和writeReplace方法在Lambda对象和序列化形式之间进行转换。
在这个过程中,序列化形式需要包含重建对象所需的所有信息,而这些信息可以通过metafactory来获得。例如,序列化形式可以如下所示:

public interface SerializedLambda extends Serializable {
    // 捕获上下文
    String getCapturingClass();

    // SAM 描述符
    String getDescriptorClass();
    String getDescriptorName();
    String getDescriptorMethodType();

    // 实现
    int getImplReferenceKind();
    String getImplClass();
    String getImplName();
    String getImplMethodType();

    // 动态参数 -- 这些也需要实现 Serializable 接口
    Object[] getCapturedArgs();
}

在这里,SerializedLambda接口提供了在原始Lambda捕获现场的所有信息。当捕获一个可序列化的Lambda时,metafactory必须返回一个实现writeReplace方法的对象,该方法返回一个SerializedLambda实现,该实现具有一个readResolve方法,负责重新创建Lambda对象。
这种序列化策略允许在序列化和反序列化之间进行灵活的切换,确保Lambda表达式在不同的实现之间保持一致性。通过这种方式,Java中的Lambda表达式不仅仅是一种强大的编程工具,还在动态翻译和序列化方面展现了其灵活性和智能性。

文章来源:https://blog.csdn.net/qcwlmqy/article/details/134958154
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。