深入理解 Lambda 表达式

简介: Java 8 的 Lambda 表达式已经不再是“新特性”。现在很多人工作中会使用 Lambda 表达式。但是,你是否真正理解 Lambda 表达式的底层原理?

一、背景

Java 8 的 Lambda 表达式已经不再是“新特性”。
现在很多人工作中会使用 Lambda 表达式。
但是,你是否真正理解 Lambda 表达式的底层原理?
在这里插入图片描述

本文给出自己的理解,希望对大家有帮助。

二、分析

下面是一段非常简单的代码,其中用到了 Stream

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

public class ListDemo {
    public static void main(String[] args) {
        List<DogDO> dogs = new ArrayList<>();

        List<String> tom = dogs.stream().filter(dog -> dog.getName().startsWith("tom")).map(dog -> dog.getName().toLowerCase()).collect(Collectors.toList());
        System.out.println(tom);
    }
}

我们使用 Jclasslib 插件(《那些相见恨晚的 IDEA插件》 中有介绍),查看字节码:
在这里插入图片描述

大家也可以自己使用 javac 和 javap 指令去在命令行执行。
如:

 javac ListDemo.java
 javap -p -s -c -v -l ListDemo

我们可以看到多出一个内部类和 BootstrapMethods

lambda$main$0

0 aload_0
1 ldc #20 <tom>
3 invokevirtual #21 <java/lang/String.startsWith : (Ljava/lang/String;)Z>
6 ireturn

在这里插入图片描述

相当于:

private static boolean lambda$main$0(String name){
   return name.startsWith("tom");
}

lambda$main$1

0 aload_0
1 invokevirtual #19 <java/lang/String.toLowerCase : ()Ljava/lang/String;>
4 areturn

在这里插入图片描述

相当于

private static String lambda$main$1(String name){
   return name.toLowerCase();
}

通过上述简单分析就可以看出来,本质上 lambda 表达式最终会被编译为私有静态方法。

main 方法

  0 new #2 <java/util/ArrayList>
 3 dup
 4 invokespecial #3 <java/util/ArrayList.<init> : ()V>
 7 astore_1
 8 aload_1
 9 ldc #4 <jam>
11 invokeinterface #5 <java/util/List.add : (Ljava/lang/Object;)Z> count 2
16 pop
17 aload_1
18 ldc #6 <tom cat>
20 invokeinterface #5 <java/util/List.add : (Ljava/lang/Object;)Z> count 2
25 pop
26 aload_1
27 ldc #7 <tom jetty>
29 invokeinterface #5 <java/util/List.add : (Ljava/lang/Object;)Z> count 2
34 pop
35 aload_1
36 ldc #8 <gom jetty>
38 invokeinterface #5 <java/util/List.add : (Ljava/lang/Object;)Z> count 2
43 pop
44 aload_1
45 invokeinterface #9 <java/util/List.stream : ()Ljava/util/stream/Stream;> count 1
50 invokedynamic #10 <test, BootstrapMethods #0>
55 invokeinterface #11 <java/util/stream/Stream.filter : (Ljava/util/function/Predicate;)Ljava/util/stream/Stream;> count 2
60 invokedynamic #12 <apply, BootstrapMethods #1>
65 invokeinterface #13 <java/util/stream/Stream.map : (Ljava/util/function/Function;)Ljava/util/stream/Stream;> count 2
70 invokestatic #14 <java/util/stream/Collectors.toList : ()Ljava/util/stream/Collector;>
73 invokeinterface #15 <java/util/stream/Stream.collect : (Ljava/util/stream/Collector;)Ljava/lang/Object;> count 2
78 checkcast #16 <java/util/List>
81 astore_2
82 getstatic #17 <java/lang/System.out : Ljava/io/PrintStream;>
85 aload_2
86 invokevirtual #18 <java/io/PrintStream.println : (Ljava/lang/Object;)V>
89 return

通过 invokedynamic 指令执行动态方法调用。

50 invokedynamic #10 <test, BootstrapMethods #0>

我们可以直接在插件上,点击命令跳转到对应官方说明文档中:
https://docs.oracle.com/javase/specs/jvms/se16/html/jvms-6.html#jvms-6.5.invokedynamic

在这里插入图片描述
从 JVM 文档 可以看到 该方法是为了实现动态调用计算,它通过常量池重点额符号引用转为动态计算调用,其中 CallSite 实例就是目标方法调用实例。

可以在插件里一直跟下去

invokedynamic #10 中的常量池中的 #10 为下面的内容:
在这里插入图片描述

其中 BootstrapMethods # 0 对应
在这里插入图片描述

可以看到这是对 java.lang.invoke.LambdaMetafactory#metafactory 的调用,返回值是 java.lang.invoke.CallSite 对象,这个对象代表了真正执行的目标方法调用。

 /**
     * Facilitates the creation of simple "function objects" that implement one
     * or more interfaces by delegation to a provided {@link MethodHandle},
     * after appropriate type adaptation and partial evaluation of arguments.
     * Typically used as a <em>bootstrap method</em> for {@code invokedynamic}
     * call sites, to support the <em>lambda expression</em> and <em>method
     * reference expression</em> features of the Java Programming Language.
     *
     * <p>This is the standard, streamlined metafactory; additional flexibility
     * is provided by {@link #altMetafactory(MethodHandles.Lookup, String, MethodType, Object...)}.
     * A general description of the behavior of this method is provided
     * {@link LambdaMetafactory above}.
     *
     * <p>When the target of the {@code CallSite} returned from this method is
     * invoked, the resulting function objects are instances of a class which
     * implements the interface named by the return type of {@code invokedType},
     * declares a method with the name given by {@code invokedName} and the
     * signature given by {@code samMethodType}.  It may also override additional
     * methods from {@code Object}.
     *
     * @param caller Represents a lookup context with the accessibility
     *               privileges of the caller.  Specifically, the lookup context
     *               must have
     *               <a href="MethodHandles.Lookup.html#privacc">private access</a>
     *               privileges.
     *               When used with {@code invokedynamic}, this is stacked
     *               automatically by the VM.
     * @param invokedName The name of the method to implement.  When used with
     *                    {@code invokedynamic}, this is provided by the
     *                    {@code NameAndType} of the {@code InvokeDynamic}
     *                    structure and is stacked automatically by the VM.
     * @param invokedType The expected signature of the {@code CallSite}.  The
     *                    parameter types represent the types of capture variables;
     *                    the return type is the interface to implement.   When
     *                    used with {@code invokedynamic}, this is provided by
     *                    the {@code NameAndType} of the {@code InvokeDynamic}
     *                    structure and is stacked automatically by the VM.
     *                    In the event that the implementation method is an
     *                    instance method and this signature has any parameters,
     *                    the first parameter in the invocation signature must
     *                    correspond to the receiver.
     * @param samMethodType Signature and return type of method to be implemented
     *                      by the function object.
     * @param implMethod A direct method handle describing the implementation
     *                   method which should be called (with suitable adaptation
     *                   of argument types, return types, and with captured
     *                   arguments prepended to the invocation arguments) at
     *                   invocation time.
     * @param instantiatedMethodType The signature and return type that should
     *                               be enforced dynamically at invocation time.
     *                               This may be the same as {@code samMethodType},
     *                               or may be a specialization of it.
     * @return a CallSite whose target can be used to perform capture, generating
     *         instances of the interface named by {@code invokedType}
     * @throws LambdaConversionException If any of the linkage invariants
     *                                   described {@link LambdaMetafactory above}
     *                                   are violated, or the lookup context
     *                                   does not have private access privileges.
     */
    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();
    }

即 lambda 表达式将代码写到私有静态方法中,然后构造目标类型的实现。

为了更直观地看到效果,大家可以在 IDEA 该类运行参数上加上

-Djdk.internal.lambda.dumpProxyClasses=你电脑上的想输出到的路径

在这里插入图片描述

可以看到编译出来的内部类:
在这里插入图片描述
可以使用 Luyten 反编译工具。
下载地址在这里:
在这里插入图片描述

查看源码:
在这里插入图片描述

在这里插入图片描述

可以看到编译器自动帮我们生成了 PredicateFunction 内部类,类名为[目标Class$$Lambda$数字] 的形式,在内部类中调用上面的静态方法。

逻辑层面等价于下面代码:

package other.list;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;

public class ListDemo {
    public static void main(String[] args) {
        List<String> dogNames = new ArrayList<>();
        dogNames.add("jam");
        dogNames.add("tom cat");
        dogNames.add("tom jetty");
        dogNames.add("gom jetty");


        List<String> tom = dogNames.stream().filter(new ListDemo$$Lambda$1()).map(new ListDemo$$Lambda$2()).collect(Collectors.toList());
        System.out.println(tom);
    }

    private static boolean lambda$min$0(String name) {
        return name.startsWith("tom");
    }

    private static String lambda$main$1(String name) {
        return name.toLowerCase();
    }

    static class ListDemo$$Lambda$1 implements Predicate<String> {
        @Override
        public boolean test(String name) {
            return lambda$min$0(name);
        }
    }

    static class ListDemo$$Lambda$2 implements Function<String, String> {
        @Override
        public String apply(String name) {
            return lambda$main$1(name);
        }
    }
}



三、拓展

有了上面的讲解,相信大家对 Lambda 表达式已经有了较深的理解。

请大家猜想并动手验证 Map.forEach 写法的底层实现是怎样的?

示例:

import java.util.HashMap;
import java.util.Map;

public class LambdaMapDemo {
    public static void main(String[] args) {
        Map<Integer, String> map = new HashMap<>();
        for (int i = 0; i < 10; i++) {
            map.put(i, String.valueOf(i));
        }

        // 底层如何实现?
        map.forEach((k, v) -> {
            System.out.println("k:" + k + " -> v:" + v);
        });
    }
}

相信大家看到下面 main 函数的字节码,应该已经可以脑补出实现方式:

 0 new #2 <java/util/HashMap>
 3 dup
 4 invokespecial #3 <java/util/HashMap.<init> : ()V>
 7 astore_1
 8 iconst_0
 9 istore_2
10 iload_2
11 bipush 10
13 if_icmpge 37 (+24)
16 aload_1
17 iload_2
18 invokestatic #4 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
21 iload_2
22 invokestatic #5 <java/lang/String.valueOf : (I)Ljava/lang/String;>
25 invokeinterface #6 <java/util/Map.put : (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;> count 3
30 pop
31 iinc 2 by 1
34 goto 10 (-24)
37 aload_1
38 invokedynamic #7 <accept, BootstrapMethods #0>
43 invokeinterface #8 <java/util/Map.forEach : (Ljava/util/function/BiConsumer;)V> count 2
48 return

请大家自己写一下逻辑上的等价代码。

希望大家可以有看到 Lambda 表达式,就可以自行脑补出底层实现的能力。

四、总结

很多知识看似习以为常,但是都可以继续深挖,可以学到不一样的知识。
只有真正搞透一个知识点面试中才会有足够的自信。

另外 Lambda 虽好,但不要“贪杯”,滥用 Lambda 对代码的可读性和可维护性都会到来挑战。

感兴趣可以继续看另外一篇文章:
《Lambda 表达式带来的复杂性的破解之道》


创作不易,如果本文对你有帮助,欢迎点赞、收藏加关注,你的支持和鼓励,是我创作的最大动力。
在这里插入图片描述
转载请注明出处: https://blog.csdn.net/w605283073/article/details/121571573
相关文章
|
关系型数据库 Linux 数据库
EMQ
|
Linux 网络性能优化
MQTT 5.0 报文解析 03:SUBSCRIBE 与 UNSUBSCRIBE
在 MQTT 中,SUBSCRIBE 报文用于发起订阅请求,SUBACK 报文用于返回订阅结果。而 UNSUBSCRIBE 和 UNSUBACK 报文则在取消订阅时使用。相比于取消订阅,订阅操作更加常用。不过在本文中,我们仍然会一并介绍订阅与取消订阅报文的结构与组成。
EMQ
661 107
MQTT 5.0 报文解析 03:SUBSCRIBE 与 UNSUBSCRIBE
|
存储 Java Docker
使用Docker部署Java应用的最佳实践
使用Docker部署Java应用的最佳实践
|
11月前
|
JavaScript Java 中间件
Java CompletableFuture 异步超时实现探索
本文探讨了在JDK 8中`CompletableFuture`缺乏超时中断任务能力的问题,提出了一种异步超时实现方案,通过自定义工具类模拟JDK 9中`orTimeout`方法的功能,解决了任务超时无法精确控制的问题,适用于多线程并行执行优化场景。
253 0
ly~
|
供应链 搜索推荐 大数据
大数据在零售业中的应用
在零售业中,大数据通过分析顾客的购买记录、在线浏览习惯等数据,帮助零售商理解顾客行为并提供个性化服务。例如,分析网站点击路径以了解顾客兴趣,并利用历史购买数据开发智能推荐系统,提升销售和顾客满意度。此外,大数据还能优化库存管理,通过分析销售数据和市场需求,更准确地预测需求,减少库存积压和缺货现象,提高资金流动性。
ly~
1038 2
|
人工智能 分布式计算 大数据
AI Native平台,跨越AI应用从创新到生产的鸿沟
2024年是AI应用的元年,以大模型为中心的 AI Native 应用大爆发正在从理想变成现实。云计算带来的应用创新潮,经历了虚拟机时代和云原生时代,正在全面拥抱以大模型为核心的 AI Native 阶段,推动大数据与AI的工作流前所未有地紧密结合。领先大模型、高效的AI计算平台和统一的大数据平台是 AI Native 应用广泛落地背后不可获缺的要素。 9月20日,2024云栖大会上,阿里云副总裁、阿里云计算平台事业部负责人汪军华宣布大数据AI平台全面升级,为 AI Native 应用大爆发提供坚实的平台支撑。
|
存储 自然语言处理 算法
使用ClickHouse进行矢量搜索 - 第一部分
本文介绍了向量搜索的概念,即使用数学向量来存储和检索数据。向量可以捕捉数据的语义关系,提高搜索效率。文章还提到了向量搜索在推荐、问题回答、图像/视频搜索等方面的应用。向量搜索可以应用于文本数据、图像数据、音频数据等不同类型的数据。最后,文章总结了向量搜索的挑战和现有技术,并展望了未来的研究方向。
47989 26
|
Linux 测试技术 开发工具
Linux文件操作之文本文件编辑与查找
在Linux系统中,文本文件编辑和查找是日常工作中经常遇到的任务。本文将详细介绍Linux下常用的文本文件编辑器和查找命令,包括vi/vim、nano等编辑器,以及grep、sed、awk等查找命令。我们将详细讲解每个工具的使用方法和常见用例,帮助读者高效处理文本文件。
918 0
|
Ubuntu Linux 数据安全/隐私保护
Linux专栏(二)——创建虚拟机与Ubuntu安装
Linux专栏(二)——创建虚拟机与Ubuntu安装
327 0
|
关系型数据库 MySQL 数据库
[ MySQL ] 使用Navicat进行MySQL数据库备份 / 还原(Part 1:备份.nb3文件方式)
本文主要讲解如何用Navicat(Navicat Premium ,或者Navicat for mysql)进行MySQL备份和恢复数据库。 本文主要大纲为:使用Navicat备份工具方式进行备份和还原,其中包括还原自身数据库和还原到其他目标库。
7751 0
[ MySQL ] 使用Navicat进行MySQL数据库备份 / 还原(Part 1:备份.nb3文件方式)

热门文章

最新文章