还不清楚 Lambda 的底层原理?一文帮你搞懂
1、写在开头
Lambda 和 Stream 是 Jdk1.8 中引入的两个重要特性。
Lambda 是函数式编程,可以将匿名方法像参数一样传递,本章节将从 4 个方面来介绍 lambda:Lambda 基础语法、Lambda 表达式的应用层面、Lambda 的字节码源码 以及 优缺点性能。
2、温习 JVM 基础
开讲前,我们先回顾下 JVM 的内存管理结构,这节我们会涉及到方法区:
程序计数器(Program Counter Register):当前线程执行的字节码指示器
Java 虚拟机栈(Java Virtual Machine Stacks):Java 方法执行的内存模型,每个方法会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
本地方法栈(Native Method Stack):(虚拟机使用到的)本地方法执行的内存模型。
Java 堆(Java Heap):虚拟机启动时创建的内存区域,唯一目的是存放对象实例,处于逻辑连续但物理不连续内存空间中。
方法区(Method Area):存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
运行时常量池(Runtime Constant Pool):方法区的一部分,存放编译器生成的各种字面值和符号引用。
另外补充常量池的项目类型知识:
另外补充字节码指令:invokedynamic 指令
在 Class 文件中,方法调用即是对常量池(ConstantPool)属性表中的一个符号引用,在类加载的解析期或者运行时才能确定直接引用。invokestatic 主要用于调用 static 关键字标记的静态方法 invokespecial 主要用于调用私有方法,构造器,父类方法。
invokevirtual 虚方法,不确定调用那一个实现类,比如 Java 中的重写的方法调用。例子可以参考:从字节码指令看重写在 JVM 中的实现 invokeinterface 接口方法,运行时才能确定实现接口的对象,也就是运行时确定方法的直接引用,而不是解析期间。
3、Lambda 是什么?
Lambda 的定义:
Lambda 表达式(lambda expression)是一个匿名函数,Lambda 表达式基于数学中的λ演算得名,直接对应于其中的 lambda 抽象(lambda abstraction),是一个匿名函数,即没有函数名的函数。
Lambda 表达式可以表示闭包(注意和数学传统意义上的不同)。
java 对 Lambda 的语法定义如下:
(parameters) -> expression
或
(parameters) ->{ statements; }
因为 lambda 本质是一个匿名函数,那么跟普通的函数方法就肯定有共同点了:入参 &方法体 &返回值。
以下是 lambda 表达式的重要特征:
可以参考下面的 Lambda 使用例子 1:
我们定义了以下的 lambda 接口
lambda 表达式实现
当然,如果觉得这种灵活的编程不太适应,那么可以用最保险的办法,那就是始终用()圈住入参,用{}圈住实现体。
4、@FunctionalInterface 注解
下面我们讲解:Lambda 的注解与应用例子。
每个 lambda 声明接口,都用到了 @FunctionalInterface,这便是 jdk8 引入的 Lambda 注解了(实际上 jdk8 没要求必须显式声明该注解)。
@FunctionalInterface 注解
举例子 2:lambda 应用实例
通过例子 2,可以知道这个 @FunctionalInterface 注解有以下特点:
该注解只能标记在”有且仅有一个抽象方法”的接口上。
JDK8 接口中的静态方法和默认方法,都不算是抽象方法。
接口默认继承 Java.lang.Object,所以如果接口显示声明覆盖了 Object 中方法,那么也不算抽象方法。
该注解不是必须的,如果一个接口符合”函数式接口”定义,那么加不加该注解都没有影响。加上该注解能够更好地让编译器进行检查。如果编写的不是函数式接口,但是加上了 @FunctionInterface,那么编译器会报错。
5、Lambda 字节码编译原理
下面我们讲解:Lambda 的 class 字节码源码,从编译角度彻底理解 lambda 的实现。
(实际上,lambda 在应用层面会这么方便,是因为编译器在底层做了大量的工作,我们可以一窥究竟。)
举例子 3:
我们基于例子 2 的 MyFunction lambda 声明接口,写了 LambdaSourceTestCase 用例:
编译与查看虚拟机运行时信息指令:
下面我们分 3 个步骤,来分析 lambda 的字节码源码。
1、在主体方法里,我们找到 lambda 表达式 匹配的 invokedynamic 指令。
2、根据 invokedynamic 指令的偏移量,定位到 #0:#23 分别指向的 bootstrap 属性表 & 常量池信息。
2.1 常量池信息(Constant Pool)
2.2 BootstrapMethods 属性表
3、字节码分析:最后的最后,是一个类的静态方法:LambdaMetafactory.metafactory()方法。
总结归纳:
lambda 表达式对应一个 incokedynamic 指令,通过指令在常量池的符号引用,可以得到 BootstrapMethods 属性表对应的引导方法。
在运行时,JVM 会通过调用这个引导方法生成一个含有 MethodHandle(CallSite 的 target 属性)对象的 CallSite 作为一个 Lambda 的回调点。Lambda 的表达式信息在 JVM 中通过字节码生成技术转换成一个内部类,这个内部类被绑定到 MethodHandle 对象中。每次执行 lambda 的时候,都会找到表达式对应的回调点 CallSite 执行。一个 CallSite 可以被多次执行(在多次调用的时候)。
在多次调用的时候,只会有一个 invokedynamic 指令,在 comparator 调用 comparator.compare 或 comparator.reversed 方法时,都会通过 CallSite 找到其内部的 MethodHandle,并通过 MethodHandle 调用 Lambda 的内部表示形式 LambdaForm。
6、Lambda 优势与劣势
总而言之,函数式编程是技术的发展方向,而 Lambda 是函数式编程最基础的内容,所以 Java 8 中加入 Lambda 表达式本身是符合技术发展方向的。
优点:
1、代码更加简洁,效率高;
2、减少匿名内部类的创建,节省资源(Lambda 的性能表现,在多数情况也比匿名内部类好,性能方面可以参考一下 Oracle 的 Sergey Kuksenko 发布的 Lambda 性能报告);
缺点:
1、不熟悉 Lambda 表达式的语法的人,不太容易看得懂;
2、虽然代码更加简洁,但可读性差,不利于维护;
7、总结
本文从 4 个方面介绍了:Lambda 基础语法、Lambda 表达式的应用层面、Lambda 的字节码源码 以及 优缺点性能,希望大家从中能有所收获。
文章参考了以下文章:
https://www.cnblogs.com/jixp/articles/10548492.html
https://www.oracle.com/technetwork/java/jvmls2013kuksen-2014088.pdf
8、延伸阅读
《源码系列》
《经典书籍》
《Java并发编程实战:第2章 影响线程安全性的原子性和加锁机制》
《Java并发编程实战:第3章 助于线程安全的三剑客:final & volatile & 线程封闭》
《服务端技术栈》
《算法系列》
《设计模式》
版权声明: 本文为 InfoQ 作者【后台技术汇】的原创文章。
原文链接:【http://xie.infoq.cn/article/a1d37acee77243dc23fffddee】。文章转载请联系作者。
评论