前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >一文带你读懂JDK源码:Lambda表达式

一文带你读懂JDK源码:Lambda表达式

作者头像
后台技术汇
发布2022-05-28 12:24:50
4010
发布2022-05-28 12:24:50
举报
文章被收录于专栏:后台技术汇

Lambda和Stream是Jdk1.8中引入的两个重要特性。

Lambda是函数式编程,可以将匿名方法像参数一样传递,本章节将从4个方面来介绍lambda:Lambda基础语法、Lambda表达式的应用层面、Lambda的字节码源码 以及 优缺点性能。

开讲前,我们先回顾下JVM的内存管理结构,这节我们会涉及到方法区:

  • 程序计数器(Program Counter Register):当前线程执行的字节码指示器
  • Java虚拟机栈(Java Virtual Machine Stacks):Java方法执行的内存模型,每个方法会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
  • 本地方法栈(Native Method Stack):(虚拟机使用到的)本地方法执行的内存模型。
  • Java堆(Java Heap):虚拟机启动时创建的内存区域,唯一目的是存放对象实例,处于逻辑连续但物理不连续内存空间中。
  • 方法区(Method Area):存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
  • 运行时常量池(Runtime Constant Pool):方法区的一部分,存放编译器生成的各种字面值和符号引用。

另外补充常量池的项目类型知识:

序号

常量池信息

备注

1

CONSTANT_InvokeDynamic_Info

标识一个动态方法调用点

2

CONSTANT_NameAndType_Info

标识字段或方法的部分符合引用

另外补充字节码指令:invokedynamic指令

在Class文件中,方法调用即是对常量池(ConstantPool)属性表中的一个符号引用,在类加载的解析期或者运行时才能确定直接引用。invokestatic 主要用于调用static关键字标记的静态方法 invokespecial 主要用于调用私有方法,构造器,父类方法。 invokevirtual 虚方法,不确定调用那一个实现类,比如Java中的重写的方法调用。例子可以参考:从字节码指令看重写在JVM中的实现 invokeinterface 接口方法,运行时才能确定实现接口的对象,也就是运行时确定方法的直接引用,而不是解析期间。

Lambda 是什么?

Lambda的定义:

Lambda 表达式(lambda expression)是一个匿名函数,Lambda表达式基于数学中的λ演算得名,直接对应于其中的lambda抽象(lambda abstraction),是一个匿名函数,即没有函数名的函数。 Lambda 表达式可以表示闭包(注意和数学传统意义上的不同)。

java对Lambda的语法定义如下:

代码语言:javascript
复制
(parameters) -> expression

代码语言:javascript
复制
(parameters) ->{ statements; }

因为lambda本质是一个匿名函数,那么跟普通的函数方法就肯定有共同点了:入参&方法体&返回值。

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

序号

描述

1

可选类型声明:不需要声明参数类型,编译器可以统一识别参数值。

2

可选的参数圆括号:一个参数无需定义圆括号,但多个参数需要定义圆括号。

3

可选的大括号:如果主体包含了一个语句,就不需要使用大括号。

4

可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定明表达式返回了一个数值。

以参考下面的Lambda使用例子1:

  • 我们定义了以下的lambda接口
代码语言:javascript
复制
  //无参但是有返回值
  @FunctionalInterface
  interface testNoArgButReturnVal {
    int testNoArgButReturnVal();
  }
  //一个参数,有返回值
  @FunctionalInterface
  interface testArgWithReturnVal {
    int testArgWithReturnVal(String message);
  }
  //两个参数,有返回值
  @FunctionalInterface
  interface testArgsWithReturnVal {
    int testArgsWithReturnVal(String message, Integer value);
  }
  //两个参数,无返回值
  @FunctionalInterface
  interface testArgsWithNoReturnVal {
    void testArgsWithNoReturnVal(String message, Integer value);
  }
  • lambda表达式实现
代码语言:javascript
复制
    //无参但是有返回值
    testNoArgButReturnVal testNoArgButReturnVal = ()->1;
    testNoArgButReturnVal testNoArgButReturnVal2 = ()->{return 1;};

    //一个参数,有返回值
    testArgWithReturnVal testArgWithReturnVal = (String a) -> {return 1;};
    testArgWithReturnVal testArgWithReturnVal2 = (a) -> {return 1;};
    testArgWithReturnVal testArgWithReturnVal3 = a -> {return 1;};
    testArgWithReturnVal testArgWithReturnVal4 = a -> 1;

    //两个参数,有返回值
    testArgsWithReturnVal testArgsWithReturnVal = (String a, Integer b) -> {return 1;};
    testArgsWithReturnVal testArgsWithReturnVal2 = (a, b) -> {return 1;};
    testArgsWithReturnVal testArgsWithReturnVal3 = (a, b) -> 1;

    //两个参数,无返回值
    testArgsWithNoReturnVal testArgsWithNoReturnVal = (String a, Integer b) -> {};
    testArgsWithNoReturnVal testArgsWithNoReturnVal2 = (String a, Integer b) -> {System.out.println("a = " + a + ", b = " + b);};

当然,如果觉得这种灵活的编程不太适应,那么可以用最保险的办法,那就是始终用()圈住入参,用{}圈住实现体。

Lambda 注解应用

下面我们讲解:Lambda的注解与应用例子。

每个lambda声明接口,都用到了 @FunctionalInterface,这便是jdk8引入的Lambda注解了(实际上jdk8没要求必须显式声明该注解)。

@FunctionalInterface 注解

代码语言:javascript
复制
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface FunctionalInterface {}

举例子2:lambda 应用实例

代码语言:javascript
复制
/**
 * <p>
 *     Java 8 中采用函数式接口作为Lambda 表达式的目标类型。
 *     函数式接口(Functional Interface)是一个有且仅有一个抽象方法声明的接口。
 *     任意只包含一个抽象方法的接口,我们都可以用来做成Lambda表达式。
 *     每个与之对应的lambda表达式必须要与抽象方法的声明相匹配。
 * </p>
 */
//自定义函数式接口时,应当在接口前加上@FunctionalInterface标注(虽然不加也不会有错误)。
//编译器会注意到这个标注,如果你的接口中定义了第二个抽象方法的话,编译器会抛出异常。
@FunctionalInterface
public interface MyFunction {
  /**
   *  函数式接口中只能有一个抽象方法(这里不包括与Object的方法重名的方法)
   *  接口中唯一抽象方法的命名并不重要,因为函数式接口就是对某一行为进行抽象,主要目的就是支持 Lambda 表达式
   */
  String print(String s);

  //Error,如果你的接口中定义了第二个抽象方法的话,编译器会抛出异常。
//  String print2(String s);
}

通过例子2,可以知道这个@FunctionalInterface 注解有以下特点:

  • 该注解只能标记在”有且仅有一个抽象方法”的接口上。
  • JDK8接口中的静态方法和默认方法,都不算是抽象方法。
  • 接口默认继承Java.lang.Object,所以如果接口显示声明覆盖了Object中方法,那么也不算抽象方法。
  • 该注解不是必须的,如果一个接口符合”函数式接口”定义,那么加不加该注解都没有影响。加上该注解能够更好地让编译器进行检查。如果编写的不是函数式接口,但是加上了@FunctionInterface,那么编译器会报错。

Lambda 字节码源码

下面我们讲解:Lambda的 class 字节码源码,从编译角度彻底理解lambda的实现。

(实际上,lambda在应用层面会这么方便,是因为编译器在底层做了大量的工作,我们可以一窥究竟。)

举例子3:

我们基于例子2的MyFunction lambda声明接口,写了 LambdaSourceTestCase 用例:

代码语言:javascript
复制
public class LambdaSourceTestCase {
  public static void main(String[] args) {
    String a = "a"; //定义局部变量
    MyFunction myFunction = (s)-> {return s;}; //定义匿名函数实现
    myFunction.print(a); //调用lambda
  }
}

编译与查看虚拟机运行时信息指令:

代码语言:javascript
复制
javac -encoding utf8 LambdaSourceTestCase.java MyFunction.java
javap -verbose LambdaSourceTestCase.class

下面我们通过3个步骤,来分析lambda的字节码源码。

1、在主体方法里,我们找到lambda表达式 匹配的 invokedynamic 指令。

2、根据invokedynamic指令的偏移量,定位到#0:#23 分别指向的bootstrap属性表 & 常量池信息。

2.1 常量池信息(Constant Pool)

2.2 BootstrapMethods 属性表

3、字节码分析:

最后的最后,是一个类的静态方法:

LambdaMetafactory.metafactory()方法。

代码语言:javascript
复制
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;

总结归纳:

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。

Lambda 优势与劣势

总而言之,函数式编程是技术的发展方向,而Lambda是函数式编程最基础的内容,所以Java 8中加入Lambda表达式本身是符合技术发展方向的。

优点:

1、代码更加简洁,效率高;

2、减少匿名内部类的创建,节省资源(Lambda的性能表现,在多数情况也比匿名内部类好,性能方面可以参考一下Oracle的Sergey Kuksenko发布的 Lambda 性能报告);

缺点:

1、不熟悉Lambda表达式的语法的人,不太容易看得懂;

2、虽然代码更加简洁,但可读性差,不利于维护;

总结

本文从4个方面介绍了:Lambda基础语法、Lambda表达式的应用层面、Lambda的字节码源码 以及 优缺点性能,希望大家从中能有所收获。

文章参考了以下文章:

https://docs.oracle.com/javase/8/docs/api/java/lang/invoke/LambdaMetafactory.html#metafactory-java.lang.invoke.MethodHandles.Lookup-java.lang.String-java.lang.invoke.MethodType-java.lang.invoke.MethodType-java.lang.invoke.MethodHandle-java.lang.invoke.MethodType-

https://www.cnblogs.com/jixp/articles/10548492.html

https://www.oracle.com/technetwork/java/jvmls2013kuksen-2014088.pdf

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-01-04,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 后台技术汇 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Lambda 是什么?
  • Lambda 注解应用
  • Lambda 字节码源码
  • Lambda 优势与劣势
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档