函数式接口原理都讲烂了,还不来了解...

简介: 大家好,函数式接口的应用篇已经给大家讲完,今天,指北君和大家一同深入探索Java实现函数式接口的原理。本篇将从编译,执行层面为大家讲解函数式接口运行的机制,让各位小伙伴更进一步加深对函数式接口的理解


概述


函数式接口包含三部分内容:

  • (应用篇一JDK源码解析——深入函数式接口(应用篇一))(1)函数式接口的来源,(2)Lambda表达式,(3)双冒号运算符
  • (应用篇二函数式编程,这样学就废了)(4)详细介绍@FunctionInterface注解(5)对java.util.function包进行解读
  • (原理篇)介绍函数式接口的实现原理 在看本篇之前,请大家对应先看应用篇一和应用篇二,本篇作为原理篇,将为大家较为深入的剖析函数式接口如何编译,JVM又是如何关联衔接各个部分的。

说明:源码使用的版本为JDK-11.0.11


编译


首先我们从编译出发,因为无论是接口还是类,都需要经过编译,然后在运行期由JVM执行调用,现在我们来看看几个关键位置的编译结果。先来看函数式接口编译

20.png

接口的编译信息中没有任何额外的工作,如果显示声明了FunctionInterface注解,则编译信息中带有,反之则无。

接下来,我们着重来看应用部分的代码编译的情况,先看应用部分的源代码:

21.png22.png


这段源码中选取了几种典型的场景进行组合,让大家了解更多的扩展知识,因此代码稍显长。

  • invokeLambda() 单个参数的lambda表达式,省略参数括号和表达式主体的花括号。
  • invokeEta() eta方式的方法引用。
  • invokeLambda2() 两个参数的lambda表达式,lambda中使用成员变量。


lambda表达式的编译


指北君和大家一起看看编译后的内容,使用命令查看编译后的方法结构(javap -p com.tree.sample.func.LambdaBinaryCode)

23.png


30.png


30.png30.png30.jpg

24.png

从编译信息中我们可以看到几条明显相同的逻辑:

  • LocalVariableTable 首先包含了函数的输入参数,并且一致
  • 24行执行String.format方法
  • 27行执行PrintStream.println方法 从上面三个关键部分我们可以确定就是invokeLambda方法中的lambda表达式编译后的内容了。

仔细的小伙伴比较  和  两个方法后,可能会发现两个问题:

1. 两个方法怎么一个是static一个是非static的
2. 方法命名中的数字为什么不是数字连续的?
对于第一个问题,比较invokeLambda和invokeLambda2的源码,小伙伴发现有什么不同么?是否可以看到invokeLambda2中的lambda表达式引用了成员属性lambdaVar。这就是lambda生成方法的一种逻辑,未使用成员变量的lambda表达式编译成静态方法,使用了成员变量的lambda语句则编译为成员方法

第二个问题我们将留待后面回答。


Lambda调用


上面我们看到了lambda表达式的代码编译成了一个独立方法,指北君继续带领大家查看编译后的文件,我们要了解编译后lambda方法是如何调用执行的。查看invokeLambda方法的编译后的内容(直贴出了关键部分):

25.png

在invokeLambda中有一个指令invokedynamic,熟悉动态语言的小伙伴可能知道,这个指令是Java7为支持动态脚本语言而增加的。而函数式Java调用函数接口也正是通过invokedynamic指令来实现的。invokeLambda的详细内容指北君后续单独为大家讲解,今天我们关注函数接口的调用过程。

使用invokeLambda指令,那么该指令是直接调用的lambda$0方法么?我们知道list.forEach(xx)调用中,我们是将函数接口作为参数传递到其他类的函数中进行执行的。Java需要解决两个问题:

1)如何将方法传递给被调用的外部类的方法。

2)外部的类和方法如何访问我们内部私有的方法。


引导方法表


为解决上面两个问题,我们继续查编译后的文件,在末尾,我们看到下面的部分:


26.png

这生成了三个引导方法,刚好和我们的三个函数接口调用一致,从引导方法的参数我们看出

27.png

顺便回答一下之前的方法名称的数字序号不连续问题,我们看出,方法名称的序号是根据引导方法的序号来确定的,不是根据生成的lambda表达式方法序号来的。我们看到,引导方法的逻辑似乎就是调用lambda方法或者其他的函数接口,每个引导方法中都出现了LambdaMetafactory.metafactory方法


动态调用


现在,我们结合invokedynamic指令来说明BootstrapMethods执行的过程


28.png

动态调用逻辑

上面的的流程显示了动态调用的基本逻辑

  1. 执行invokedynamic
  2. 检查调用点是否已连接可用

  1. 如果未连接,构建动态调用点
  2. 执行引导方法
  3. 生成并加载调用点对应的动态内部类
  4. 连接

  1. 调用动态内部类方法
  2. 内部类调用lambda对应的方法并执行

这两个阶段我们通过调用堆栈也能明显观察到:


29.jpg

                                                            引导阶段

30.jpg                                                     执行阶段

我们还可以通过设置VM参数-Djdk.internal.lambda.dumpProxyClasses,查看以引导阶段动态生成的内部类:


31.jpg

                                               动态内部类列表

打开其中一个如下:

32.jpg

                                                    动态内部类详情

小结


动态函数接口的调用原理,指北君就给大家介绍到这里了,相信大家看完本篇内容后,对函数式接口有了更深一层的学习。由于涉及的内容较多,没有时间给大家逐一详细的给每个涉及到的类进行解读。后续指北君会根据小伙伴们需要对今天提及的知识点做深入的阶段,比如invokeddynamic指令,class结构,动态调用相关的各部分代码逻辑。

相关文章
|
3月前
|
设计模式 缓存 Java
死磕-高效的Java编程(一)
死磕-高效的Java编程(一)
|
3月前
|
Java 程序员 编译器
死磕-高效的Java编程(二)
死磕-高效的Java编程(二)
|
4月前
|
设计模式 程序员
故意把代码写得很烂,这样的 “防御性编程“ 可取吗?
故意把代码写得很烂,这样的 “防御性编程“ 可取吗?
|
4月前
|
存储 Java
八股day03_方法
八股day03_方法
|
7月前
|
设计模式 Java 开发者
一目了然!谁能想到Java多线程设计模式竟然被图解,看完不服不行
多线程设计模式在Java编程中起着至关重要的作用,它能够有效提高程序的执行效率,使得程序在处理大量数据和复杂任务时更加高效。然而,对于初学者来说,理解和应用多线程设计模式可能是一项相当具有挑战性的任务。为了让读者更加轻松地掌握这一复杂主题,我们带着一种全新的图解方式,深入剖析Java多线程设计模式的精髓。
aviator 属实搞事情
aviator 属实搞事情
101 0
|
设计模式 XML JavaScript
写了这么久代码你了解Java面向对象的设计原则吗?(一)
写了这么久代码你了解Java面向对象的设计原则吗?
126 0
写了这么久代码你了解Java面向对象的设计原则吗?(一)
|
存储 XML Java
写了这么久代码你了解Java面向对象的设计原则吗?(二)
写了这么久代码你了解Java面向对象的设计原则吗
224 0
写了这么久代码你了解Java面向对象的设计原则吗?(二)
|
存储 Java 数据库连接
写了这么久代码你了解Java面向对象的设计原则吗?(三)
写了这么久代码你了解Java面向对象的设计原则吗?
133 0
写了这么久代码你了解Java面向对象的设计原则吗?(三)
|
Python
又烧脑又炫技还没什么用,在代码里面打印自身
又烧脑又炫技还没什么用,在代码里面打印自身
213 0
又烧脑又炫技还没什么用,在代码里面打印自身