精品国产人成在线_亚洲高清无码在线观看_国产在线视频国产永久2021_国产AV综合第一页一个的一区免费影院黑人_最近中文字幕MV高清在线视频

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

    文章

    8245

    瀏覽量

    149924
  • 函數
    +關注

    關注

    3

    文章

    4233

    瀏覽量

    61961
  • 編譯
    +關注

    關注

    0

    文章

    646

    瀏覽量

    32662
  • JVM
    JVM
    +關注

    關注

    0

    文章

    155

    瀏覽量

    12168
收藏 人收藏

    評論

    相關推薦

    如何查看及更改函數/函數塊的調用環境

    模塊化設計的思想是把一些相似的功能(比如電機控制、閥控制)設計成函數函數塊,這樣就可以反復調用。其優點是:使程序架構更加清晰,避免重復編寫相似功能的代碼。不過可能會產生一個疑惑:既然PLC的程序
    的頭像 發表于 11-17 09:08 ?662次閱讀
    如何查看及更改<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 ?3517次閱讀

    嵌入式軟件架構設計之函數調用

    函數調用很好理解,即使剛學沒多久的朋友也知道函數調用是怎么實現的,即調用一個已經封裝好的函數,實
    的頭像 發表于 02-15 14:48 ?987次閱讀
    嵌入式軟件架構設計之<b class='flag-5'>函數</b><b class='flag-5'>調用</b>

    什么是函數調用?

    函數調用,就是使用我們已經定義好的函數,或者C語言自帶的庫函數。
    的頭像 發表于 04-04 17:21 ?5179次閱讀

    SCL中調用函數的示例

    在此,可插入函數 (FC) 調用函數塊 (FB) 調用。函數塊可作為單實例、多重實例或參數實例進行調用
    的頭像 發表于 06-06 10:18 ?1864次閱讀

    觸發器的輸出是現態函數

    觸發器的輸出是現態函數 觸發器是數字電路中的一種重要元件,它們通常被用于存儲和裝載二進制數據,也可以用于控制和同步各種數字電路。在許多數字電路應用中,觸發器的輸出通常被用作輸入信號來觸發后續電路。在
    的頭像 發表于 08-24 15:50 ?870次閱讀

    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 ?1010次閱讀

    python函數函數之間的調用

    函數函數之間的調用 3.1 第一種情況 程序代碼如下: def x ( f ): def y (): print ( 1 ) return y def f (): print ( 2 )x(f
    的頭像 發表于 10-04 17:17 ?496次閱讀