写点什么

Lambda 应用介绍及实现原理剖析

  • 2023-04-20
    北京
  • 本文字数:3746 字

    阅读完需:约 12 分钟

Lambda 应用介绍及实现原理剖析

hello,大家好,我是张张,「架构精进之路」公号作者。

1、Lamdba 简介

Lambda 表达式是 JDK8 的一个新特性,初次接触 Lambda,感觉和 PHP 的函数式编程,俗称闭包大同小异。以前在写 PHP 的时候,在方法中为了复用,但又没必要重新写个新的方法的时候,我们可以定义一个局部变量来定义一段匿名函数,实现方法内的代码复用。

允许把函数作为一个方法的参数,即行为参数化,函数作为参数传递进方法中。相比于以往臃肿复杂的代码。

我们使用 Lambda 表达式具有很多好处:

  • 取代大部分的匿名内部类,写出更优雅的 Java 代码,尤其在集合的遍历和其他集合操作中,可以极大地优化代码结构;

  • 可以简洁代码,提高代码的可读性;

  • 简化数据类型 在 Lambda 表达式中可以将参数的数据类型省略,只留下一个数据名称。


2、核心原则

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

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

格式说明:

  1. 小括号内的语法与传统方法参数列表一致:无参数则留空;多个参数则用逗号分隔。

  2. -> 是新引入的语法格式,代表指向动作。

  3. 大括号内的语法与传统方法体要求基本一致。


我们在日常使用 Lambda 表达式的时候,其实只需记住核心 6 个原则:可推导可省略,具体表现在:

  • 参数类型可以省略(推导出来)

  • 方法体只有一句代码 可以省略{},return 和分号

  • 方法只有一个参数的时候 小括号可以省略


3、日常应用

Lambda 表达式在我们日常开发的应用场景非常广泛,下面来简单举一些常见的例子:

3.1 列表迭代

public class LamTest {  /**	 * Lambda 表达式应用前	 */	public void lamBefore() {		List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);		for(int element : numbers) {			System.out.println(element);		}	}
/** * Lambda 表达式应用后 */ public void lamAfter() { List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); numbers.forEach(System.out::println); }
}
复制代码


3.2 Map 映射

/** * Map 映射应用 */public void lamMap() {  List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);  List<Integer> mapped = numbers.stream().map(x -> x*2).collect(Collectors.toList());  mapped.forEach(System.out::println);}
复制代码


3.3 Reduce 聚合

/** * Reduce 聚合应用 */public void lamReduce() {  List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);  int sum = numbers.stream().reduce((x,y)-> x+y).get();  System.out.println(sum);}
复制代码


4、方法引用

方法引用是一个语法糖,可以用来简化开发。


在我们使用 Lambda 表达式的时候,如果 “->” 的右边要执行的表达式只是调用一个类已有的方法,那么就可以用「方法引用」来替代 Lambda 表达式。


4.1 引用静态方法

当我们要执行的表达式是调用某个类的静态方法,并且这个静态方法的参数列表和接口里抽象函数的参数列表一一对应时,我们可以采用引用静态方法的格式。

假如 Lambda 表达式符合如下格式:

([变量1, 变量2, ...]) -> 类名.静态方法名([变量1, 变量2, ...])
复制代码

我们可以简写成如下格式:类名::静态方法名

例如:

Collections.sort(list, (o1, o2) -> Utils.compare(o1, o2));
复制代码


如果是方法引用的话,可以简写成这样:

Collections.sort(list, Utils::compare);
复制代码


4.2 引用对象的方法

当我们要执行的表达式是调用某个对象的方法,并且这个方法的参数列表和接口里抽象函数的参数列表一一对应时,我们就可以采用引用对象的方法的格式。  

假如 Lambda 表达式符合如下格式:

([变量1, 变量2, ...]) -> 对象引用.方法名([变量1, 变量2, ...])
复制代码

我们可以简写成如下格式:对象引用::方法名

例如:

public class MyClass {
   public int compare(Integer o1, Integer o2) {
       return o1.compareTo(o2);
   }
}
复制代码


当我们创建一个该类的对象,并在 Lambda 表达式中使用该对象的方法时,一般可以这么写:

MyClass myClass = new MyClass();
Collections.sort(list, (o1, o2) -> myClass.compare(o1, o2));
复制代码


那么采用方法引用的方式,可以这样简写:

MyClass myClass = new MyClass(); Collections.sort(list, myClass::compare);
复制代码


4.3 引用类的方法

引用类的方法所采用的参数对应形式与上两种略有不同。如果 Lambda 表达式的 “->” 的右边要执行的表达式是调用的 “->” 的左边第一个参数的某个实例方法,并且从第二个参数开始(或无参)对应到该实例方法的参数列表时,就可以使用这种方法

假如 Lambda 表达式符合如下格式:

(变量1[, 变量2, ...]) -> 变量1.实例方法([变量2, ...])
复制代码

我们可以简写成如下格式:变量 1 对应的类名::实例方法名

例如:

Collections.sort(list, (o1, o2) -> o1.compareTo(o2));
复制代码


按照上面的说法,就可以简写成这样:

Collections.sort(list, Integer::compareTo);
复制代码


5、实现原理

经过上面的介绍,我们看到 Lambda 表达式只是为了简化匿名内部类书写,看起来似乎在编译阶段把所有的 Lambda 表达式替换成匿名内部类就可以了。但实际情况并非如此,在 JVM 层面,Lambda 表达式和匿名内部类其实有着明显的差别。

5.1 匿名内部类的实现

匿名内部类仍然是一个类,只是不需要我们显式指定类名,编译器会自动为该类取名。比如有如下形式的代码:

public classLamTest {    public static voidmain(String[] args) {        newThread(newRunnable() {           @Override            public voidrun() {                System.out.println("lambda test");           }       }).start();   }}
复制代码


编译之后将会产生两个 class 文件:LamTest.class  LamTest$1.class

使用 javap -c LamTest.class 进一步分析 LamTest.class 的字节码,部分结果如下:


可以发现在 4: new #3 这一行创建了匿名内部类的对象。

5.2 Lambda 表达式的实现

接下来我们将上面的示例代码使用 Lambda 表达式实现,代码如下:

public classLamTest {    public static voidmain(String[] args) {        newThread(()->System.out.println("lambda test")).start();   }}
复制代码


此时编译后只会产生一个文件 LamTest.class,再来看看通过 javap 对该文件反编译后的结果。


从上面的结果我们发现 Lambda 表达式被封装成了主类的一个私有方法,并通过 invokedynamic 指令进行调用。

其实在设计 Lambda 表达式的解析方案时,主要考虑下面两点:

  • 可扩展性,不把解析方案写死在字节码上;

  • 解析 Lambda 表达式后,对字节码文件干扰尽量降到最低,起码保证一定的可读性。


正是基于以上两点考虑, invokedynamic 指令被运用到解析 Lambda 表达式,优势如下:

  • 字节码表示简单,利用 invokedynamic 指令本身的特性(引导方法),把 Lambda 表达式在字节码文件的表示和真正的解析分离;

  • Lambda 表达式的链接解析发生运行期而非编译期,由 invokedynamic 的引导方法执行,扩展性强。


Lambda 是面向方法接口编程,其实我更多认为是 JDK8 提供的语法糖,因为对非方法引用的 Lambda 表达式,编译器都会为其生成一个方法实现 Lambda 表达式的逻辑,并出现在编译后的字节码文件中。这个方法的生成是 Lambda 表达式解析的关键,Lambda 表达式的解析分成三个阶段:

  • 链接阶段, 负责生成动态调用点

  • 变量捕获,闭包中的变量访问

  • Lambda 表达式调用


链接阶段由 invokedynamic 指令后面跟随的 Bootstrap Method 引导方法完成,生成动态调用点。我们先来看 Lambda 表达式解析用的最多的一个引导方法:


java.lang.invoke.LambdaMetafactory#metaFactory:


该启动方法以及参数都是编译器指定,下面来解释下这几个参数的含义:

  • caller 指的是 MethodHandle,lookupClass 指向 enclosing object

  • invokedName 需要实现的函数式接口方法名

  • invokedType 变量捕获列表

  • samMethodType 需要实现的函数式接口方法签名(sam 是 single abstract method )

  • implMethod 指向生成的 desugaring method

  • instantiatedMethodType 对应运行时 sam 的方法签名,运行时签名验证,在非泛型情况下,和 samMethodType 相同;比如下面代码中,samMethodType 是(Ljava/lang/Object)V, instantiatedMethodType 则是(Ljava/lang/String)V


引导方法 metaMethod 根据这些参数生成 java.lang.invoke.CallSite 动态调用点对象支持 Lambda 表达式的调用执行。在引导方法中会动态生成一个模板匿名类,查看这个类的字节码可以通过加上 -Djdk.internal.lambda.dumpProxyClasses 参数指定 dump 的目录,该匿名是一个模板类,其特点如下:

  • 实现函数式接口,内部逻辑很简单,调用上面提到的脱糖方法(desugaring method);

  • 生成一个构造方法, 构造方法参数为被捕获参数变量,所有变量存储为类实例变量;

  • 如果被捕获参数变量列表不为空, 则会生成一个工厂方法 get$lambad,方法签名和构造方法相同;该工厂方法的作用是避免重复调用 asm 生成匿名类字节码,提升性能;在下一节的性能分析中会提到这个方法的作用。


因此,我们可以得出结论:Lambda 表达式是通过 invokedynamic 指令实现的,并且书写 Lambda 表达式不会产生新的类。  


希望今天的讲解对大家有所帮助,谢谢!

Thanks for reading!

作者:张张,十年研发风雨路,大厂架构师,「架构精进之路」专注架构技术沉淀学习及分享,职业与认知升级,坚持分享接地气儿的干货文章,期待与你一起成长。

关注并私信我回复“01”,送你一份程序员成长进阶大礼包,欢迎勾搭。


发布于: 刚刚阅读数: 4
用户头像

🏆 InfoQ写作平台-签约作者 🏆 2018-02-26 加入

同名微信公众号「架构精进之路」,专注软件架构研究,技术学习与职业成长!坚持原创总结、沉淀和分享,希望能带给大家一些引导和启发,感谢各位的支持(关注、点赞、分享)!

评论

发布
暂无评论
Lambda 应用介绍及实现原理剖析_Java_架构精进之路_InfoQ写作社区