深入探索Java 8 Lambda表達式

来源:http://www.cnblogs.com/bymax/archive/2016/03/04/5242913.html
-Advertisement-
Play Games

2014年3月,Java 8發佈,Lambda表達式作為一項重要的特性隨之而來。或許現在你已經在使用Lambda表達式來書寫簡潔靈活的代碼。比如,你可以使用Lambda表達式和新增的流相關的API,完成如下的大量數據的查詢處理: int total = invoices.stream() .filt


2014年3月,Java 8發佈,Lambda表達式作為一項重要的特性隨之而來。或許現在你已經在使用Lambda表達式來書寫簡潔靈活的代碼。比如,你可以使用Lambda表達式和新增的流相關的API,完成如下的大量數據的查詢處理:


int total = invoices.stream()
                    .filter(inv -> inv.getMonth() == Month.JULY)
                    .mapToInt(Invoice::getAmount)
                    .sum();

上面的示例代碼描述瞭如何從一打發票中計算出7月份的應付款總額。其中我們使用Lambda表達式過濾出7月份的發票,使用方法引用來提取出發票的金額。

到這裡,你可能會對Java編譯器和JVM內部如何處理Lambda表達式和方法引用比較好奇。可能會提出這樣的問題,Lambda表達式會不會就是匿名內部類的語法糖呢?畢竟上面的示例代碼可以使用匿名內部類實現,將Lambda表達式的方法體實現移到匿名內部類對應的方法中即可,但是我們並不贊成這樣做。如下為匿名內部類實現版本:


int total = invoices.stream()
                    .filter(new Predicate<Invoice>() {
                        @Override
                        public boolean test(Invoice inv) {
                            return inv.getMonth() == Month.JULY;
                        }
                    })
                    .mapToInt(new ToIntFunction<Invoice>() {
                        @Override
                        public int applyAsInt(Invoice inv) {
                            return inv.getAmount();
                        }
                    })
                    .sum();

本文將會介紹為什麼Java編譯器沒有採用內部類的形式處理Lambda表達式,並解密Lambda表達式和方法引用的內部實現。接著介紹位元組碼生成並簡略分析Lambda表達式理論上的性能。最後,我們將討論一下實踐中Lambda表達式的性能問題。

為什麼匿名內部類不好?

實際上,匿名內部類存在著影響應用性能的問題。

首先,編譯器會為每一個匿名內部類創建一個類文件。創建出來的類文件的名稱通常按照這樣的規則 ClassName符合和數字。生成如此多的文件就會帶來問題,因為類在使用之前需要載入類文件併進行驗證,這個過程則會影響應用的啟動性能。類文件的載入很有可能是一個耗時的操作,這其中包含了磁碟IO和解壓JAR文件。

假設Lambda表達式翻譯成匿名內部類,那麼每一個Lambda表達式都會有一個對應的類文件。隨著匿名內部類進行載入,其必然要占用JVM中的元空間(從Java 8開始永久代的一種替代實現)。如果匿名內部類的方法被JIT編譯成機器代碼,則會存儲到代碼緩存中。同時,匿名內部類都需要實例化成獨立的對象。以上關於匿名內部類的種種會使得應用的記憶體占用增加。因此我們有必要引入新的緩存機制減少過多的記憶體占用,這也就意味著我們需要引入某種抽象層。

最重要的,一旦Lambda表達式使用了匿名內部類實現,就會限制了後續Lambda表達式實現的更改,降低了其隨著JVM改進而改進的能力。

我們看一下下麵的這段代碼:


import java.util.function.Function;
public class AnonymousClassExample {
    Function<String, String> format = new Function<String, String>() {
        public String apply(String input){
            return Character.toUpperCase(input.charAt(0)) + input.substring(1);
        }
    };
}

使用這個命令我們可以檢查任何類文件生成的位元組碼


javap -c -v ClassName

示例中使用Function創建的匿名內部類對應的位元組碼如下:


0: aload_0 
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0 
5: new #2 // class AnonymousClassExample$1
8: dup 
9: aload_0 
10: invokespecial #3 // Method AnonymousClass$1."<init>":(LAnonymousClassExample;)V
13: putfield #4 // Field format:Ljava/util/function/Function;
16: return

上述位元組碼的含義如下:

  • 第5行,使用位元組碼操作new創建了類型AnonymousClassExample$1的一個對象,同時將新創建的對象的的引用壓入棧中。
  • 第8行,使用dup操作複製棧上的引用。
  • 第10行,上面的複製的引用被指令invokespecial消耗使用,用來初始化匿名內部類實例。
  • 第13行,棧頂依舊是創建的對象的引用,這個引用通過putfield指令保存到AnonymousClassExample類的format屬性中。

AnonymousClassExample1這個類文件,你會發現這個類就是Function介面的實現。

將Lambda表達式翻譯成匿名內部類會限制以後可能進行的優化(比如緩存)。因為一旦使用了翻譯成匿名內部類形式,那麼Lambda表達式則和匿名內部類的位元組碼生成機制綁定。因而,Java語言和JVM工程師需要設計一個穩定並且具有足夠信息的二進位表示形式來支持以後的JVM實現策略。下麵的部分將介紹不使用匿名內部類機制,Lambda表達式是如何工作的。

Lambdas表達式和invokedynamic

為瞭解決前面提到的擔心,Java語言和JVM工程師決定將翻譯策略推遲到運行時。利用Java 7引入的invokedynamic位元組碼指令我們可以高效地完成這一實現。將Lambda表達式轉化成位元組碼只需要如下兩步:

1.生成一個invokedynamic調用點,也叫做Lambda工廠。當調用時返回一個Lambda表達式轉化成的函數式介面實例。

2.將Lambda表達式的方法體轉換成方法供invokedynamic指令調用。

為了闡明上述的第一步,我們這裡舉一個包含Lambda表達式的簡單類:


import java.util.function.Function;

public class Lambda {
    Function<String, Integer> f = s -> Integer.parseInt(s);
}

查看上面的類經過編譯之後生成的位元組碼:


0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: invokedynamic #2, 0 // InvokeDynamic
                  #0:apply:()Ljava/util/function/Function;
10: putfield #3 // Field f:Ljava/util/function/Function;
13: return

需要註意的是,方法引用的編譯稍微有點不同,因為javac不需要創建一個合成的方法,javac可以直接訪問該方法。

Lambda表達式轉化成位元組碼的第二步取決於Lambda表達式是否為對變數捕獲。Lambda表達式方法體需要訪問外部的變數則為對變數捕獲,反之則為對變數不捕獲。

對於不進行變數捕獲的Lambda表達式,其方法體實現會被提取到一個與之具有相同簽名的靜態方法中,這個靜態方法和Lambda表達式位於同一個類中。比如上面的那段Lambda表達式會被提取成類似這樣的方法:


static Integer lambda$1(String s) {
    return Integer.parseInt(s);
}

需要註意的是,這裡的$1並不是代表內部類,這裡僅僅是為了展示編譯後的代碼而已。

對於捕獲變數的Lambda表達式情況有點複雜,同前面一樣Lambda表達式依然會被提取到一個靜態方法中,不同的是被捕獲的變數同正常的參數一樣傳入到這個方法中。在本例中,採用通用的翻譯策略預先將被捕獲的變數作為額外的參數傳入方法中。比如下麵的示例代碼:


int offset = 100;
Function<String, Integer> f = s -> Integer.parseInt(s) + offset;

對應的翻譯後的實現方法為:


static Integer lambda$1(int offset, String s) {
    return Integer.parseInt(s) + offset;
}

需要註意的是編譯器對於Lambda表達式的翻譯策略並非固定的,因為這樣invokedynamic可以使編譯器在後期使用不同的翻譯實現策略。比如,被捕獲的變數可以放入數組中。如果Lambda表達式用到了類的實例的屬性,其對應生成的方法可以是實例方法,而不是靜態方法,這樣可以避免傳入多餘的參數。

性能分析

Lambda表達式最主要的優勢表現在性能方面,雖然使用它很輕鬆的將很多行代碼縮減成一句,但是其內部實現卻不這麼簡單。下麵對內部實現的每一步進行性能分析。

第一步就是連接,對應的就是我們上面提到的Lambda工廠。這一步相當於匿名內部類的類載入過程。來自Oracle的Sergey Kuksenko發佈過相關的性能報告,並且他也在2013 JVM語言大會就該話題做過分享。報告表明,Lambda工廠的預熱準備需要消耗時間,並且這個過程比較慢。伴隨著更多的調用點連接,代碼被頻繁調用後(比如被JIT編譯優化)性能會提升。另一方面如果連接處於不頻繁調用的情況,那麼Lambda工廠方式也會比匿名內部類載入要快,最高可達100倍。

第二步就是捕獲變數。正如我們前面提到的,如果是不進行捕獲變數,這一步會自動進行優化,避免在基於Lambda工廠實現下額外創建對象。對於匿名內部類而言,這一步對應的是創建外部類的實例,為了優化內部類這一步的問題,我們需要手動的修改代碼,如創建一個對象,並將它設置給一個靜態的屬性。如下述代碼:


// Hoisted Function
public static final Function<String, Integer> parseInt = new Function<String, Integer>() {
    public Integer apply(String arg) {
        return Integer.parseInt(arg);
    }
}; 

// Usage:
int result = parseInt.apply(“123”);

第三部就是真實方法的調用。在這一步中匿名內部類和Lambda表達式執行的操作相同,因此沒有性能上的差別。不進行捕獲的Lambda表達式要比進行static優化過的匿名內部類較優。進行變數捕獲的Lambda表達式和匿名內部類表達式性能大致相同。

在這一節中,我們明顯可以看到Lambda表達式的實現表現良好,匿名內部類通常需要我們手動的進行優化來避免額外對象生成,而對於不進行變數捕獲的Lambda表達式,JVM已經為我們做好了優化。

實踐中的性能分析

理解了Lambda的性能模型很是重要,但是實際應用中的總體性能如何呢?我們在使用Java 8 編寫了一些軟體項目,一般都取得了很好的效果。非變數捕獲的Lambda表達式給我們帶來了很大的幫助。這裡有一個很特殊的例子描述了關於優化方向的一些有趣的問題。

這個例子的場景是代碼需要運行在一個要求GC暫定時間越少越好的系統上。因而我們需要避免創建大量的對象。在這個工程中,我們使用了大量的Lambda表達式來實現回調處理。然而在這些使用Lambda實現的回調中很多並沒有捕獲局部變數,而是需要引用當前類的變數或者調用當前類的方法。然而目前仍需要對象分配。下麵就是我們提到的例子的代碼:


public MessageProcessor() {} 

public int processMessages() {
    return queue.read(obj -> {
        if (obj instanceof NewClient) {
            this.processNewClient((NewClient) obj);
        } 
        ...
    });
}

有一個簡單的辦法解決這個問題,我們將Lambda表達式的代碼提前到構造方法中,並將其賦值給一個成員屬性。在調用點我們直接引用這個屬性即可。下麵就是修改後的代碼:


private final Consumer<Msg> handler; 

public MessageProcessor() {
    handler = obj -> {
        if (obj instanceof NewClient) {
            this.processNewClient((NewClient) obj);
        }
        ...
    };
} 

public int processMessages() {
    return queue.read(handler);
}

然而上面的修改後代碼給卻給整個工程帶來了一個嚴重的問題:性能分析表明,這種修改產生很大的對象申請,其產生的記憶體申請在總應用的60%以上。

類似這種無關上下文的優化可能帶來其他問題。

  1. 純粹為了優化的目的,使用了非慣用的代碼寫法,可讀性會稍差一些。
  2. 記憶體分配方面的問題,示例中為MessageProcessor增加了一個成員屬性,使得MessageProcessor對象需要申請更大的記憶體空間。Lambda表達式的創建和捕獲位於構造方式中,使得MessageProcessor的構造方法調用緩慢一些。

我們遇到這種情況,需要進行記憶體分析,結合合理的業務用例來進行優化。有些情況下,我們使用成員屬性確保為經常調用的Lambda表達式只申請一個對象,這樣的緩存策略大有裨益。任何性能調優的科學的方法都可以進行嘗試。

上述的方法也是其他程式員對Lambda表達式進行優化應該使用的。書寫整潔,簡單,函數式的代碼永遠是第一步。任何優化,如上面的提前代碼作為成員屬性,都必須結合真實的具體問題進行處理。變數捕獲並申請對象的Lambda表達式並非不好,就像我們我們寫出new Foo()代碼並非一無是處一樣。

除此之外,我們想要寫出最優的Lambda表達式,常規書寫很重要。如果一個Lambda表達式用來表示一個簡單的方法,並且沒有必要對上下文進行捕獲,大多數情況下,一切以簡單可讀即可。

總結

在這片文章中,我們研究了Lambda表達式不是簡單的匿名內部類的語法糖,為什麼匿名內部類不是Lambda表達式的內部實現機制以及Lambda表達式的具體實現機制。對於大多數情況來說,Lambda表達式要比匿名內部類性能更優。然而現狀並非完美,基於測量驅動優化,我們仍然有很大的提升空間。

Lambda表達式的這種實現形式並非Java 8 所有。Scala曾經通過生成匿名內部類的形式支持Lambda表達式。在Scala 2.12版本,Lambda的實現形式替換為Java 8中的Lambda 工廠機制。後續其他可以在JVM上運行的語言也可能支持Lambda的這種機制。

原文:Java 8 Lambdas - A Peek Under the Hood

查看英文原文:Java 8 Lambdas - A Peek Under the Hood



您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 在WPF中顯示一張圖片,本是一件再簡單不過的事情。一張圖片,一行XAML代碼即可。 但是前段時間遇到了一件奇怪的事: 開發機上運行正常的程式,在某些客戶機器上卻顯示不了圖片,而且除了這個問題,其它運行情況都正常。開始排查問題吧,先檢查代碼,然後檢查編譯打包過程,並沒有發現任何問題。再然後去客戶機器上
  • using Microsoft.AspNet.Identity; public ActionResult AddRole(String name){ using (var roleManager = new RoleManager<IdentityRole>(new RoleStore<Identi
  • 在C#中,如果要實現兩個列表的左鏈接查詢,我們的一般用法就是用的linq表達式就是 List<Pet> pets = new List<Pet>{ new Pet { Name="Barley", Age=8 }, new Pet { Name="Boots", Age=4 }, new Pet {
  • 本人也尚在學習使用之中,錯誤之處請大家指正。 開發環境:vs2015 UP1 項目環境:asp.net 4.6.1 模板為:asp.net 5 模板 identity版本為:asp.net identity 3.0.0 如圖: 建成後的項目已經和之前的模板建成的項目有非常大的不同了。identity
  • 每次寫分頁導航的時候都要在html頁面寫一堆標簽和樣式,太麻煩了,所以乾脆自己動手封裝一個自己喜歡的類直接生成。 一、PageHelper類: /// <summary> /// 分頁導航 /// </summary> /// <param name="pageNum">當前第幾頁</param>
  • 好久沒有寫博客了,這段時間準備寫一下字元串函數 QQ群: 499092562;歡迎交流 字元串函數: 1、LEN(需要獲取長度的字元串) 返回:字元串的長度 示例: SELECT LEN('小搬運工很帥!') 2、RIGHT(需要被從右邊截取的字元串,截取的開始下標,截取的長度) 返回:右邊的字元串
  • http://www.ourd3js.com/wordpress/
  • Lua中的協程和多線程很相似,每一個協程有自己的堆棧,自己的局部變數,可以通過yield-resume實現在協程間的切換。不同之處是:Lua協程是非搶占式的多線程,必須手動在不同的協程間切換,且同一時刻只能有一個協程在運行。並且Lua中的協程無法在外部將其停止,而且有可能導致程式阻塞。 協同程式(C
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...