本文的脉络
Lambda介绍
何为lambda
咱们首先来说说Lambda这个名字,Lambda并不是一个什么的缩写,它是希腊第十一个字母λ的读音,同时它也是微积分函数中的一个概念,所表达的意思是一个函数入参和出参定义,在编程语言中其实是借用了数学中的 λ,并且多了一点含义,在编程语言中功能代表它具体功能的叫法是匿名函数(Anonymous Function),根据百科的解释:
匿名函数(英语:Anonymous Function)在计算机编程中是指一类无需定义标识符(函数名)的函数或子程序。
接着再来说说Lambda 的历史,虽然它在 JDK8 发布之后才正式出现,但是在编程语言界,它是一个具有悠久历史的东西,最早在 1958 年在Lisp 语言中首先采用,而且虽然Java脱胎于C++,但是C++在2011年已经发布了Lambda 了,但是 JDK8 的 LTS 在2014年才发布,所以 Java 被人叫做老土不是没有原因的,现代编程语言则是全部一出生就自带 Lambda 支持,所以Lambda 其实是越来越火的一个节奏~
Lambda 在编程语言中往往是一个匿名函数,也就是说Lambda 是一个抽象概念,而编程语言提供了配套支持,比如在 Java 中其实为Lambda 进行配套的就是函数式接口,通过函数式接口生成匿名类和方法进行Lambda 式的处理。
那么,既然是这一套规则我们明白了,那么Lambda 所提供的好处在Java中就是函数式接口所提供的能力了,函数式接口往往则是提供了一些通用能力,这些函数式接口在JDK中也有一套完整的实践,那就是 Stream。
不同语言中的Lambda
Python
例子:
C++
C++11中增加了对lambda表达式的支持
具体语法:
[1]:Lambda表达式的引入标志,在‘[]’里面可以填入‘=’或‘&’表示该lambda表达式“捕获”(lambda表达式在一定的scope可以访问的数据)的数据时以什么方式捕获的,‘&’表示一引用的方式;‘=’表明以值传递的方式捕获,除非专门指出。
[2]:Lambda表达式的参数列表
[3]:Mutable 标识
[4]:异常标识
[5]:返回值
[6]:“函数”体,也就是lambda表达式需要进行的实际操作。
例子:
Javascript
例子:
Java Lambda 表达式
Lambda 表达式在 Java 8 中添加的。
lambda 表达式是一小段代码,它接受参数并返回一个值。Lambda 表达式类似于方法,但它们不需要名称,并且可以直接在方法体中实现。
句法
最简单的 lambda 表达式包含一个参数和一个表达式:
零参数:
一个参数:
多个参数:
上面的表达式有一定的限制。它们要么返回一个值要么执行一段方法,并且它们不能包含变量、赋值或语句,例如if or for 。为了进行更复杂的操作,可以使用带有花括号的代码块。如果 lambda 表达式需要返回一个值,那么代码块应该有一个return语句。
方法引用
类 :: 静态方法
对象 :: 实例方法
构造器 :: new
原生函数式接口
@FunctionalInterface注解
有且只有一个抽象方法的接口被称为函数式接口,函数式接口适用于函数式编程的场景,Lambda就是Java中函数式编程的体现,可以使用Lambda表达式创建一个函数式接口的对象,一定要确保接口中有且只有一个抽象方法,这样Lambda才能顺利的进行推导。
与@Override 注解的作用类似,Java 8中专门为函数式接口引入了一个新的注解:@FunctionalInterface 。该注解可用于一个接口的定义上,一旦使用该注解来定义接口,编译器将会强制检查该接口是否确实有且仅有一个抽象方法(equal和hashcode方法不算),否则将会报错。但是这个注解不是必须的,只要符合函数式接口的定义,那么这个接口就是函数式接口。
Consumer: 消费性接口
Consumer通过名字可以看出它是一个消费函数式接口,主要针对的是消费(1..n 入参, 无返回)这个场景,它的代码定义如下:
通过泛型 T 定义了一个入参,但是没有返回值,它代表你可以针对这个入参做一些自定义逻辑,比较典型的例子是 forEach 方法。
例子:
Supplier: 供给型接口
Supplier通过名字比较难看出来它是一个场景的函数式接口,它主要针对的是说获取(无入参,有返回)这个场景,它的代码定义如下:
通过泛型 T 定义了一个返回值类型,但是没有入参,它代表你可以针对调用方获取某个值,比较典型的例子是 Stream 中的 collect 方法,通过自定义传入我们想要取得的某种对象进行对象收集。
例子:
Function: 函数型接口
Function 接口的名字不太能轻易看出来它的场景,它主要针对的则是转换(有入参,有返回,其中T是入参,R是返回)这个场景,其实说转换可能也不太正确,它是一个覆盖范围比较广的场景,你也可以理解为扩展版的Consumer,接口定义如下:
通过一个入参 T 进行自定义逻辑处理,最终得到一个出参 R,比较典型的例子是 Stream 中的 map 系列方法和 reduce 系列方法。
例子:
Predicate: 断言型接口
Predicate主要针对的是判断(有入参,有返回,凡是返回的类型固定为Boolean。可以说Function 是包含Predicate的 )这个场景,它的代码定义如下:
通过泛型 T 定义了一个入参,返回了一个布尔值,它代表你可以传入一段判断逻辑的函数,比较典型的例子是 Stream 中的 filter方法。
Stream表达式
Stream,就是JDK8又依托于函数式编程特性为集合类库做的一个类库,它其实就是jdk提供的函数式接口的最佳实践。它能让我们通过lambda表达式更简明扼要的以流水线的方式去处理集合内的数据,可以很轻松的完成诸如:过滤、分组、收集、归约这类操作。
其中Stream的操作大致分为两类
中间型操作
终结型操作
其中转换型操作又分为有状态和无状态两类。有状态是本次的结果需要依赖于前面的处理结果,而无状态则是不依赖。简单来讲就是无状态方法可以互相调换位置,而有状态方法不能调换位置。
中间型操作
中间型操作就是返回值依旧是stream类型的方法。api如下:
终结型操作
终结型操作与中间型相反,返回值是非Stream类型的。api如下:
探究lambda运行的底层原理
源码分析
接下来通过一个例子Debug来探究下lambda运行的底层原理,实验代码如下:
list.stream()
list.stream()最终调用了ReferencePipeline.Head,返回一个Head对象。Head是ReferencePipeline的内部类。官方注释说此类是ReferencePipeline的源阶段。也是stream调用的起始阶段。
运行完这一方法返回ReferencePipeline.Head对象,对象的所有元素保存在sourceSpliterator中
stream.filter(e -> e > 2)
filter的方法原型如下:
StatelessOp是stream 无状态的基类,与之相对的是StatefulOp,stream有状态的基类。元素原型如下E_IN是上游元素的类型,E_OUT是当前阶段返回的类型。
需要注意的是 filter等方法的构造方法:
new StatelessOp
P_OUT>(this,StreamShape.REFERENCE,StreamOpFlag.NOT_SIZED)
会将this传入。StatulessOp的构造方法,会一直super到AbstractPipeline方法。注意到AbstractPipeline类的构造方法中打注释的地方。
简化就是双向链表加入节点的操作。
运行完返回如下:注意看上一步对象的地址保存在当前对象的perviousStage中,而且当前对象增加predicate对象
如图所示:
filterStream.sorted()
和filter操作一样,将Sorted节点加入链表中同时设置标志位:
StreamOpFlag.IS_ORDERED|StreamOpFlag.IS_SORTED
运行完这一步结果如图
sortedStream.map(e -> e * 2)
map()方法跟filter()方法的执行逻辑很像,分析方法跟分析filter()方法一样
不过与filter不同时的参数中增加了mapper参数,类型为function
执行完如图所示:
mapStream.collect(Collectors.toSet());
collect方法原型和Collectors.toSet()方法原型如下:
在collect方法中会判断是否为并行流,不是的话会执行evaluate(ReduceOps.makeRef(collector)); ReduceOps.makeRef(collector)会返回类型为TerminalOp的参数,在evaluate方法中会将链表的节点都包装为Sink。
执行完如图:
关键在于上面的copyInfo方法,此方法是stream的启动方法。遍历元素调用第一节点的逻辑(filter)。然后在end方法中调用第二个节点的begin方法,begin方法又会调用第二个节点的逻辑,之后和第一个节点一样,调用end方法,触发第三个节点的begin方法..... 最后调用到最后一个节点将处理好的元素收集起来。
下面是最后一步map的节点的栈帧和运行数据和对应的方法。这一步结束后会将此次运行的stream元素都add到hashSet中。
downstream.accept - > Set::add;
mapper.apply -> e -> e * 2;
最后调用:
(Supplier)(helper.wrapAndCopyInto(makeSink(), spliterator)).get()方法将保存元素的容器获取出来。
并发流源码分析
修改代码增加并发流:
根据非并发流的分析直接来到最后一步collect。分歧在evaluate方法中,之前调用terminalOp.evaluateSequential,并发流则会调用terminalOp.evaluateParallel。
在evaluateParallel返回会执行ReduceTask累的构造方法,查看ReduceTask类发现继承AbstractTask类
继续往上查看
通过idea工具可以更直观的查看继承关系,ReduceTask最终继承ForkJoinTask。ForkJoinTask与ForkJoinPool线程有关系。
程序继续运行会调用new ReduceTask(this, helper, spliterator).invoke(),invoke方法ForkJoinTask的启动方法。
最后跟着调用链回来到AbstractTask类中的compute方法
在调用taskToFork.fork()前查看下当前变量表:
taskToFork的具体内容如下:op属性是Collectors.toSet(),而之前对元素的处理方法都在helper字段中
执行fork会把所有当前的task(this)放在ForkJoinPool这个线程池中。
执行完查看Debug线程堆栈信息,看到除了main线程在运行外,同时还多了ForkJoinPool这个线程组
同时这个又会运行到之前调用过的doExec()方法中,以此形成将大任务分解成小任务的循环。
最后主线程再执行task.doLeaf()运行到定义好的方法lambda方法中,并将此结果收集到一起
最后简单介绍下ForkJoinPool的运行原理:
ForkJoinPool核心思想:分治
总结一下并行流的和串行流的区别:串行流是一个一个处理的,而并行流是把元素先分成n部分,然后给将这n部分放到一个线程池中执行,等各个线程把这n部分执行完毕后在将结果汇总起来在输出最终结果。并行流可以极大的加速stream的处理速度,不过需要注意的是,程序中的是各个并行流公用一个线程池。
JVM分析
先写一段简单的包含lambda的代码,编译后查看编译文件和字节码。
main方法字节码如下:
同时在生成的字节码中有一个名为lambda$main$0的方法,字节码如下:
先看main方法的字节码的第29行与第35行分别是invokedynamic #5 和 invokedynamic #9
这两个字节码分别对应class文件的BootstrapMethods中。查看编译出来class文件的BootstrapMethods,有几个关键的地方,第一个是innerClasser。第二个是在BootstrapMethods出现:
java/lang/invoke/LambdaMetafactory.metafactory
debug断点调试,会运行到BootstrapMethodInvoker的127,会执行MethodHandler的invokeExact方法,此方法是native方法。
最后通过jvm的解析转发调用会来到LambdaMetafactory的metafactory方法中
这方法的最后3个入参类型就是从class文件中看到那三个入参的类型。
同时jvm也通过调用者和方法名称以及方法描述符找到了最后需要调用的方法。
查看ImplMethod参数
继续查看栈帧发现此方法是由Jvm调用而来,metafactory的上一个方法是invokeStatic当时行号是-1所以说明是jvm内部方法
可以理一下整个流程。
首先jvm启动,运行方法, 发现字节码是中存在invokeDynamic,通过invokedynamic字节码对应的BootstrapMethods调用MethodHandles.lookup方法寻找调用类中与当前lambda对应的静态内部类方法,最后生成CallSite 调用点,最后调用真正的lambda方法。
通过javap -v -p [生成的文件] 可查看相应字节码,下图就是生成的两个静态内部类
filter(x -> x > 2) 中的 x-> x > 2的字节码:
forEach(System.out::println) 中的 System.out::println 字节码:
最后因为System.out::println属于类 :: 静态方法的形式,所以在生成的字节码中存在“适配器”,即先将System.out通过静态方法赋值给对应的静态内部类,在通过调用lambda方法使用。
而正常的方法会直接调用Lambda中的lambda$main$0方法。
Lambda的序列化原理
lambda的本质在上面的探究中我们也能看到是静态内部类。序列化lambda跟序列化其他对象一样必须要实现Serializable接口, 为什么必须实现Serializable才能进行序列化呢,可以从源码中找到答案,ObjectOutputStream中的1178行。程序判断当前对象obj是否为Serializable子类,如果是的话进行序列化,否则抛出异常。注:如果需要实例化对象,那么这个对象里面的所有属性必须都是可实例化(即所有的属性包括自身都必须实现Serializable接口)。
知道序列化原理后,可以使用下列代码进行测试,并将运行时的class文件dump到本地。
利用查看dump下的class文件,发现类实现Serializable接口,在类中又增加了writeReplace方法。且方法返回值为SerializedLambda。
先看一下writeReplace方法是什么,熟悉序列化的人知道如果类在进行序列化的时候会先查询类中是否有writeReplace方法。这一点同样可以在ObjectOutputStream类中1126行找到对应的处理逻辑。调用writeReplace方法返回的对象会替换原有的obj对象。
就是说实现了Serializable接口的lambda对象最后会被实例化成SerializedLambda类型,从SerializedLambda类上面的注释中可以看出来,类中的readResolve方法会去"capturing class"中寻找$deserializeLambda$(SerializedLambda)方法并会将"this"对象当做第一个参数传入。实现$deserializeLambda$的 Lambda 类负责验证SerializedLambda的属性是否与该类实际捕获的 lambda 一致。关键字:验证
通过查看字节码发现原方法中增加$deserializeLambda$方法,字节码如下,注意是字节码第50行出现了调用lambda的invokedynamic字节码,而50行之前的字节码通过不断调用invokevirtual获取SerializedLambda的各种属性,并使用equals方法对获取到的属性做校验。根据SerializedLambda类注释的关键字在结合字节码可知$deserializeLambda$主要做校验使用。
再看一下SerializedLambda中的readResolve方法,通过capturingClass获取$deserializeLambda$方法,最后在进行调用。
最后在整理下整个lambda序列化流程,首先是对应的lambda表达式必须实现Serializable接口,在实现Serializable接口后,jvm运行时候会在lambda生成的静态内部类中增加writeReplace方法,并在调用的类中增加$deserializeLambda$方法校验使用,在序列化过程中ObjectOutputStream会调用writeReplace方法,将整个lambda表达式转换成SerializedLambda,最后将SerializedLambda类序列化保存。
最后明白了lambda的序列化过程后可以用一个例子模拟lambda的反序列化的过程,首先序列化的对象并非是原来的lambda表达式,而是SerializedLambda对象,通过调用$deserializeLambda$方法生成校验SerializedLambda方队,校验通过则调用对应的Booststrap方法进行一系列转化继续来到LambdaMetaFactory的altMetafactory方法并最终在此方法生成最后的调用CallSite
查看栈帧方法由$deserializeLambda$调用至 ,
最后调用到事先定义好的方法。
领取专属 10元无门槛券
私享最新 技术干货