0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

动态函数接口的调用原理

科技绿洲 来源:Java技术指北 作者:Java技术指北 2023-10-13 11:27 次阅读

本篇将从编译,执行层面为大家讲解函数式接口运行的机制,让各位小伙伴更进一步加深对函数式接口的理解

概述

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

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

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

编译

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

Classfile /O:/SCM/ws-java/sample-lambda/bin/com/tree/sample/func/IFuncInterfaceSample.class
  Last modified 2021-6-4; size 238 bytes
  MD5 checksum 58a3c8c5cbe9c7498e86d4a349554ae0
  Compiled from "IFuncInterfaceSample.java"
public interface com.tree.sample.func.IFuncInterfaceSample
  minor version: 0
  major version: 55
  flags: ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT
Constant pool:
   #1 = Class              #2             // com/tree/sample/func/IFuncInterfaceSample
   #2 = Utf8               com/tree/sample/func/IFuncInterfaceSample
   #3 = Class              #4             // java/lang/Object
   #4 = Utf8               java/lang/Object
   #5 = Utf8               func1
   #6 = Utf8               ()V
   #7 = Utf8               SourceFile
   #8 = Utf8               IFuncInterfaceSample.java
   #9 = Utf8               RuntimeVisibleAnnotations
  #10 = Utf8               Ljava/lang/FunctionalInterface;
{
  public abstract void func1();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_ABSTRACT
}
SourceFile: "IFuncInterfaceSample.java"
RuntimeVisibleAnnotations:
  0: #10()

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

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

public class LambdaBinaryCode {
    private int lambdaVar = 100;
    
    public static void main(String[] args) {
        
        LambdaBinaryCode ins = new LambdaBinaryCode();
        ins.invokeLambda();
        ins.invokeEta();
        ins.invokeLambda2();
    }
    
    /**
     * 简单的函数式编程示例
     */
    public void invokeLambda() {
        // 准备测试数据
        Integer[] data = new Integer[] {1, 2, 3};
        List< Integer > list = Arrays.asList(data);
        
        // 简单示例:打印List数据
        list.forEach(x - > System.out.println(String.format("Cents into Yuan: %.2f", x/100.0)));
    }
    
    /**
     * 简单的函数式编程示例
     */
    public void invokeEta() {
        // 准备测试数据
        Integer[] data = new Integer[] {1, 2, 3};
        List< Integer > list = Arrays.asList(data);
        
        // 通过eta操作符访问
        list.forEach(System.out::println);
    }
    
    /**
     * 简单的函数式编程示例
     */
    public void invokeLambda2() {
        // 准备测试数据
        Map< Integer, Integer > map = new HashMap< Integer, Integer >();
        int count = 10;
        Random r = new Random();
        while(count-- >0) {
            map.put(r.nextInt(100), r.nextInt(10000));
        }
        
        // Lambda调用示例
        map.forEach((x, y) - > {
            System.out.println(String.format("Map key: %1s, value: %2s", x, y+lambdaVar));
        });
    }
}

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

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

lambda表达式的编译

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

Compiled from "LambdaBinaryCode.java"
public class com.tree.sample.func.LambdaBinaryCode {
  private int lambdaVar;
  public com.tree.sample.func.LambdaBinaryCode();
  public static void main(java.lang.String[]);
  public void invokeLambda();
  public void invokeEta();
  public void invokeLambda2();
  private static void lambda$0(java.lang.Integer);
  private void lambda$2(java.lang.Integer, java.lang.Integer);
}

小伙伴有没发现,class文件中比源码文件中多出了两个方法:lambda2。这两个方法分别对应invokeLambda和invokeLambda2中的的lambda表达式。

我们在javap命令中增加-v参数,可以查看到增加的 方法的更多细节,不熟悉JVM指令的小伙伴也不用担心,我们只是验证 就是invokeLambda中lambda表达式对应“x -> System.out.println(String.format("Cents into Yuan: %.2f", x/100.0))”。

private static void lambda$0(java.lang.Integer);
    descriptor: (Ljava/lang/Integer;)V
    flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
    Code:
      stack=9, locals=1, args_size=1
         0: getstatic     #61                 // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #105                // String Cents into Yuan: %.2f
         5: iconst_1
         6: anewarray     #3                  // class java/lang/Object
         9: dup
        10: iconst_0
        11: aload_0
        12: invokevirtual #107                // Method java/lang/Integer.intValue:()I
        15: i2d
        16: ldc2_w        #111                // double 100.0d
        19: ddiv
        20: invokestatic  #113                // Method java/lang/Double.valueOf:(D)Ljava/lang/Double;
        23: aastore
        24: invokestatic  #118                // Method java/lang/String.format:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;
        27: invokevirtual #124                // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        30: return
      LineNumberTable:
        line 30: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      31     0     x   Ljava/lang/Integer;

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

  • 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方法的编译后的内容(直贴出了关键部分):

public void invokeLambda();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=4, locals=4, args_size=1
        ... ...
        32: istore_3
        33: aload_2
        34: invokedynamic #45,  0             // InvokeDynamic #0:accept:()Ljava/util/function/Consumer;
        39: invokeinterface #49,  2           // InterfaceMethod java/util/List.forEach:(Ljava/util/function/Consumer;)
        ... ...

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

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

引导方法表

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

BootstrapMethods:
  0: #146 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;
    Method arguments:
      #148 (Ljava/lang/Object;)V
      #151 invokestatic com/tree/sample/func/LambdaBinaryCode.lambda$0:(Ljava/lang/Integer;)V
      #152 (Ljava/lang/Integer;)V
  1: #146 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;
    Method arguments:
      #153 (Ljava/lang/Object;)V
      #156 invokevirtual java/io/PrintStream.println:(Ljava/lang/Object;)V
      #157 (Ljava/lang/Integer;)V
  2: #146 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;
    Method arguments:
      #159 (Ljava/lang/Object;Ljava/lang/Object;)V
      #162 invokespecial com/tree/sample/func/LambdaBinaryCode.lambda$2:(Ljava/lang/Integer;Ljava/lang/Integer;)V
      #163 (Ljava/lang/Integer;Ljava/lang/Integer;)V
InnerClasses:
     public static final #169= #165 of #167; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles

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

序号调用调用类型
0lambda$0 static
1PrintStream.printlnvertual
2lambda$2 special

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

动态调用

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

图片
动态调用逻辑

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

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

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

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

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

图片引导阶段

图片
执行阶段

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

图片
动态内部类列表

打开其中一个如下:

图片
动态内部类详情

小结

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

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 接口
    +关注

    关注

    33

    文章

    8575

    浏览量

    151015
  • 函数
    +关注

    关注

    3

    文章

    4327

    浏览量

    62569
  • 编译
    +关注

    关注

    0

    文章

    657

    浏览量

    32852
  • JVM
    JVM
    +关注

    关注

    0

    文章

    158

    浏览量

    12220
收藏 人收藏

    评论

    相关推荐

    如何查看及更改函数/函数块的调用环境

    模块化设计的思想是把一些相似的功能(比如电机控制、阀控制)设计成函数函数块,这样就可以反复调用。其优点是:使程序架构更加清晰,避免重复编写相似功能的代码。不过可能会产生一个疑惑:既然PLC的程序
    的头像 发表于 11-17 09:08 938次阅读
    如何查看及更改<b class='flag-5'>函数</b>/<b class='flag-5'>函数</b>块的<b class='flag-5'>调用</b>环境

    Linux系统动态库与静态库函数的使用介绍

    ,N是库的副版本号。当然也可以不要版本号,但名字必须有。相对于静态函数库,动态函数库在编译的时候并没有被编译进目标代码中,你的程序执行到相关函数时才调用
    发表于 07-04 05:33

    Linux下静态库和动态库的制作与使用

    什么是静态函数库?动态函数库又是什么?linux静态函数库是怎样创建并使用的?动态函数库是怎样创建并使用的?
    发表于 04-26 06:45

    如何创建linux静态函数库?怎么使用?

    如何创建linux静态函数库?怎么使用?
    发表于 04-27 06:58

    C++教程之函数的递归调用

    C++教程之函数的递归调用 在执行函数 f 的过程中,又要调用 f 函数本身,称为函数的递归
    发表于 05-15 18:00 35次下载

    动态Feign的“万能”接口调用

    对于fegin调用,我们一般的用法都是为每个微服务都创建对应的feignclient接口,然后为每个微服务的controller接口,一一编写对应的方法,去调用对应微服务的
    发表于 12-26 11:42 3775次阅读

    嵌入式软件架构设计之函数调用

    函数调用很好理解,即使刚学没多久的朋友也知道函数调用是怎么实现的,即调用一个已经封装好的函数,实
    的头像 发表于 02-15 14:48 1099次阅读
    嵌入式软件架构设计之<b class='flag-5'>函数</b><b class='flag-5'>调用</b>

    什么是函数调用

    函数调用,就是使用我们已经定义好的函数,或者C语言自带的库函数
    的头像 发表于 04-04 17:21 5807次阅读

    SCL中调用函数的示例

    在此,可插入函数 (FC) 调用函数块 (FB) 调用函数块可作为单实例、多重实例或参数实例进行调用
    的头像 发表于 06-06 10:18 2174次阅读

    触发器的输出是现态函数

    触发器的输出是现态函数 触发器是数字电路中的一种重要元件,它们通常被用于存储和装载二进制数据,也可以用于控制和同步各种数字电路。在许多数字电路应用中,触发器的输出通常被用作输入信号来触发后续电路。在
    的头像 发表于 08-24 15:50 1003次阅读

    Vivado ML版中动态函数交换的技术进步

    电子发烧友网站提供《Vivado ML版中动态函数交换的技术进步.pdf》资料免费下载
    发表于 09-14 09:32 0次下载
    Vivado ML版中<b class='flag-5'>动态函数</b>交换的技术进步

    隔离设计流程+动态函数交换示例

    电子发烧友网站提供《隔离设计流程+动态函数交换示例.pdf》资料免费下载
    发表于 09-14 09:31 0次下载
    隔离设计流程+<b class='flag-5'>动态函数</b>交换示例

    使用抽象外壳进行动态函数交换的解决方案效率

    电子发烧友网站提供《使用抽象外壳进行动态函数交换的解决方案效率.pdf》资料免费下载
    发表于 09-13 17:10 0次下载
    使用抽象外壳进行<b class='flag-5'>动态函数</b>交换的解决方案效率

    python定义函数调用函数的顺序

    定义函数调用函数的顺序 函数被定义后,本身是不会自动执行的,只有在被调用后,函数才会被执行,得
    的头像 发表于 10-04 17:17 1380次阅读

    python函数函数之间的调用

    函数函数之间的调用 3.1 第一种情况 程序代码如下: def x ( f ): def y (): print ( 1 ) return y def f (): print ( 2 )x(f
    的头像 发表于 10-04 17:17 585次阅读