GTLC全球技术领导力峰会·上海站,首批讲师正式上线! 了解详情
写点什么

Lambda 表达式介绍和底层实现分析

2021 年 1 月 21 日

Lambda表达式介绍和底层实现分析

如果你的需求需要匿名类来实现,例如是一个只有一个方法的接口,那么匿名类的语法可能看起来比较笨拙和不清晰,尽管匿名类比命名类更简洁,但对于只有一个方法的类来说,即使是匿名类也显得有些麻烦。还有在一些情况下,需要将功能作为参数传递给另一个方法,例如当有人单击页面上按钮时应该采取什么操作,javascript 可以通过闭包实现。在 java 语言中,lambda 表达式能够将功能视为方法参数,或将代码视为数据,而且 lambda 表达式可以更紧凑地表达单方法类的实例,在 Swing 编程和集合(Collections)编程中优势很明显。


lambda 表达式

lambda 表达式,也被称为闭包,它是推动 Java 8 发布的最重要新特性。lambda 允许把函数作为一个方法的参数(函数作为参数传递进方法中)。


lambda 表达式的语法格式如下:


(parameters) -> expression或 (parameters) ->{ statements; }
复制代码


以下是 lambda 表达式的重要特征:

例如下面是一些 lambda 表达式

(int x, int y) -> x + y() -> 42(String s) -> { System.out.println(s); }
复制代码

函数式接口

只有一个抽象方法的接口,称为函数式接口。例如下面的接口,


public interface MyFunctionInterface<T> {    public T getValue(T t);}
复制代码


测试类如下


public class MyFunctionInterfaceTest {    public static void main(String[] args) {        testMethod("   aaaa  ", s -> s.trim());        testMethod("   aaaa  ", s -> s.trim().toUpperCase());    }
public static void testMethod(String str, MyFunctionInterface<String> functionInterface) { System.out.println(functionInterface.getValue(str)); }}
复制代码


输出结果如下:

aaaaAAAA
复制代码


修改一下上面的接口,增加一个方法。


public interface MyFunctionInterface<T> {    public T getValue(T t);    public T returnValue(T t);}
复制代码


增加一个方法之后,上面就不是函数式接口了,可以看到 lambda 表达式就会报错。



虽然不能在函数式接口中定义多个方法,但可以定义默认方法、静态方法以及 java.lang.Object 里的 public 方法。如下面的代码


@FunctionalInterfacepublic interface MyFunctionInterface<T> {
public T getValue(T t);
default void defaultMethod() { System.out.println("this is default method"); }
static void staticMethod() { System.out.println("this is static method"); }

public boolean equals(Object obj);}
复制代码


我们可以在接口上使用 @FunctionalInterface 注解,如果使用 Intellij IDEA 可以在编码的时候就看见报错了,这样做可以检查它是否是一个函数式接口。



通过反编译,可以看到函数式接口其实就是一个普通的 java 接口类,如下图



函数式接口可以作为方法参数传递 lambda 表达式,但是为了将 Lambda 表达式作为参数传递,接收 Lambda 表达式的参数类型必须是与该 Lambda 表达式兼容的函数式接口的类型。但是我们没必要为每一个 lambda 表达式创建接口,在 jdk 的 java.util.function 包下面已经为我们创建了常用的函数式接口,其中比较核心的是消费型接口(Consumer<T>),供给型接口(Supplier<T>),断言型接口(Predicate<T>),函数型接口(Function<T, R>)四个接口,能够满足大部分应用场景。


lambda 表达式原理分析


继续使用上面的测试代码,可以在 IDEA 中使用 jclasslib Bytecode viewer 插件查看 MyFunctionInterface.class 源文件。

安装完 jclasslib Bytecode viewer,会在 view 菜单中出现如下两个选项



选择需要反编译的 class 文件,点击 Show Bytecode with Jclasslib 选项会出现如下界面



可以看到里面编译器多生成了 lambda$main$0 和 lambda$main$1 两个私有静态方法,属性当中多了 InnerClasses。我们可以通过 Show Bytecode 查看一下测试类字节码更详细的反编译结果,找到这两个静态方法。


// access flags 0x100Aprivate static synthetic lambda$main$1(Ljava/lang/String;)Ljava/lang/String; L0  LINENUMBER 6 L0  ALOAD 0  INVOKEVIRTUAL java/lang/String.trim ()Ljava/lang/String;  INVOKEVIRTUAL java/lang/String.toUpperCase ()Ljava/lang/String;  ARETURN L1  LOCALVARIABLE s Ljava/lang/String; L0 L1 0  MAXSTACK = 1  MAXLOCALS = 1
// access flags 0x100Aprivate static synthetic lambda$main$0(Ljava/lang/String;)Ljava/lang/String; L0 LINENUMBER 5 L0 ALOAD 0 INVOKEVIRTUAL java/lang/String.trim ()Ljava/lang/String; ARETURN L1 LOCALVARIABLE s Ljava/lang/String; L0 L1 0 MAXSTACK = 1 MAXLOCALS = 1}
复制代码


可以看到两个私有的静态方法干的就是 Lambda 表达式里面的内容,那么又是如何调用的生成的私有静态方法呢?如下图,通过分析 main 方法的 L0,首先通过 INVOKEDYNAMIC 指令调用是 MyFunctionInterface 的 getValue 方法的引用,以及后面的 BootstrapMethods #0。使用 jclasslib Bytecode viewer 查看。



点击 #3,进入下面界面



点击 BootstrapMethods #0,进入如下界面



点击 cp_info #44,进入如下界面



继续点击相应的方法描述符,我们可以看到最后



cp_info #74 内容如下:


(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;
复制代码


可以看到 INVOKEDYNAMIC 后面的一系列指令,最后使用 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;
复制代码


查看 LambdaMetafactory.metafactory 的方法,里面通过 InnerClassLambdaMetafactory 生成了 CallSite 的子类 ConstantCallSite,当通过指令调用 CallSite 会返回函数式接口的实例,而生成接口实例的方式是通过内部类的方式,由于方法比较深,就不继续贴代码了。


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();}
复制代码



CallSite 持有 com/para/lambda/MyFunctionInterfaceTest.lambda$main$0 方法的句柄,这个句柄会调用该方法。


所以使用 lambda 表达式的地方,会在类编译的时候在本类中生成对应的私有静态方法和一个 INNERCLASS 的访问标识(具体是什么东西没找到资料,注释显示是一个访问标识),该访问标识会调用引导类加载器动态生成内部类,该内部类实现了函数式接口,在实现接口的方法中,会调用编译器生成静态方法,在使用 lambda 表达式的地方,通过传递内部类实例,来调用函数式接口方法。


总结

本文从 lambda 表达式、函数式接口的介绍和对 lambda 表达式底层原理的分析来认识 java 中的函数式编程。函数式接口本身就是一个普通的接口,而 lambda 表达式本质上和匿名内部类是一样的,只不过条件更加苛刻。使用 lamda 表达式可以以一种更优雅的方式来编程。


本文转载自:360 技术(ID:qihoo_tech)

原文链接:Lambda表达式介绍和底层实现分析

2021 年 1 月 21 日 07:00953

评论

发布
暂无评论
  • JVM 是如何实现反射的?

    反射是Java语言中一个相当重要的特性,它允许正在运行的Java程序观测,甚至是修改程序的动态行为。

    2018 年 8 月 6 日

  • Android ClassLoader 加载过程源码分析

    本文从Android中ClassLoader等相关类的源码入手介绍动态加载类的原理。

  • 第 6 讲 | 动态代理是基于什么原理?

    如何分类Java语言呢?通常认为,Java是静态的强类型语言,但是因为提供了类似反射等机制,也具备了部分动态类型语言的能力。

    2018 年 5 月 17 日

  • Java SE 8: Lambda 表达式

    Java SE 8在6月13的版本中已经完全了全部的功能。在这些新的功能中,lambda表达式是推动该版本发布的最重要新特性。因为Java第一次尝试引入函数式编程的相关内容。社区对于lambda表达式也期待已久。Lambda表达式的相关内容在JSR 335中定义,本文的内容基于最新的规范和JDK 8 Build b94。 开发环境使用的是Eclipse。

  • 基础语法与类型变量:Dart 是如何表示信息的?

    我将通过一个示例,带你学习Dart的基本语法和类型系统,来帮助你在已有编程语言经验的基础上快速上手。

    2019 年 7 月 11 日

  • 观察者模式(下):如何实现一个异步非阻塞的 EventBus 框架?

    在平时的业务开发中,我们要善于抽象非业务的、可复用的功能,并积极地把它们实现成通用的框架。

    2020 年 3 月 13 日

  • 继承和多态:面向对象运行期的动态特性

    我们要想深刻理解面向对象的特征,就必须了解子类型的原理和运行期的机制。

    2019 年 9 月 11 日

  • 闭包: 理解了原理,它就不反直觉了

    闭包很有用,对面试者来讲,它几乎是前端面试必问的一个问题,比如如何用闭包特性实现面向对象编程?

    2019 年 9 月 4 日

  • Java 8 vs. Scala 之 Lambda 表达式

    2014年3月份众人期待已久的Java 8发布了,新版本从语言、编译器、类库和工具等方面对Java进行了诸多改进与提升,一时间风光无限;而JVM体系的另一门语言Scala则因为融合了函数式编程语言与面向对象编程语言的优点,从诞生以来就一直备受瞩目,迅速赢得了社区的强烈支持。两门语言孰优孰劣或许不能简单地做出定论,这取决于具体的应用场景、资源约束以及团队偏好等因素,但是无论作何选择首先都需要对它们有深入的了解,本文来自于Zappos公司Hussachai Puripunpinyo在Dzone上发表的一篇文章,介绍了他自己对Java和Scala Lambda表达式的看法。

  • 架构师训练营 - 命题作业 第 11 周

    一、导致系统不可用的原因有哪些?保障系统稳定高可用的方案有哪些?请分别列举并简述。

    2020 年 8 月 27 日

发现更多内容

LeetCode 1339. Maximum Product of Splitted Binary Tree

隔壁小王

算法

2020年6月5日 继承

瑞克与莫迪

修改git里commit信息用户名

张张张小烦

[kube 022] 混沌测试框架-Litmus

zbyufei

Kubernetes 云原生 混沌工程 Litmus litmuschaos

日志过滤

HU

一文带你了解 Kafka 原理

cxuan

kafka

如何通过众包应用本地化获得 500 万次下载?

葛仲君

android 翻译 本地化 产品开发 Play商店

ARTS week3

丽子

架构师应该具备哪些思维模型?

奈学教育

架构师

彻底搞懂 etcd 系列文章(一):初识 etcd

aoho

架构 云原生 etcd

Go: Go 调度器的任务窃取(Work-Stealing)

陈思敏捷

go golang 源码分析 原理 队列

2020年5月北京BGP机房网络质量评测报告

博睿数据

网络 服务器 存储 机房 主机

大话设计模式 | 1 简单工厂模式

Puran

C# 设计模式 PlantUML

真诚的回报

zhoo299

生活 随想

江湖事儿 | 技术人如何做好晋升准备

哈利迪

android 职业成长

像运营公司一样去做产品

胖鱼2号

创业 产品 产品经理 企业

Pycharm社区版安装教程(永久免费,随时升级)

早睡蟒

JDK 8,该离开的时候,请别留恋!

范学雷

Java 架构 编程语言 Java 25 周年

彻底搞懂 etcd 系列文章(三):etcd 集群运维部署

aoho

架构 云原生 etcd

硬不硬你说了算!近 40 张图解被问千百遍的 TCP 三次握手和四次挥手面试题

小林coding

TCP 网络安全 网络编程 面试题 计算机网络

怎样成为解决问题的高手

落曦

ARTS - Week Three

shepherd

开源 互联网 算法

我体验了一把自由职业,比 996 苦多了...

非著名程序员

创业 程序员 自由职业 创业心态

ARTS|Week 2 PlantUML 的学习和分享

Puran

LeetCode arts PlantUML

游戏夜读 | 神话故事和世界观

game1night

程序员可迁移技能的培养

MavenTalker

程序员 程序员素养 程序员成长

ARTS Week2

时之虫

ARTS 打卡计划

原创 | TDD工具集:JUnit、AssertJ和Mockito (十九)编写测试-依赖注入\测试接口\重复测试

编程道与术

Java 编程 TDD 单元测试 JUnit

如何辨别有发展潜力的员工​

Neco.W

工作 招聘

Spring 源码学习 - 单例bean的实例化过程

公众号:好奇心森林

区块链能够防伪?你彷佛有什么误解!

CECBC区块链专委会

CECBC 区块链技术 商品溯源 防伪

DNSPod与开源应用专场

DNSPod与开源应用专场

Lambda表达式介绍和底层实现分析-InfoQ