ASE'21:Java語言中Lambda表達式的誤用分析
JDK8被認為是近年來Java最重要的一次更新。其引入的一系列函數式的編程特性,特別是lambda表達式,使這一典型的面向對象編程的語言具有了函數式編程范式。Java lambda表達式由三部分構成:參數列表、箭頭字符和lambda body。參數列表類似常規方法中的形參列表,lambda body是一個表達式。在Java中,lambda表達式可以賦值給變量,也可以作為參數傳遞給方法(對應的類為函數接口)。圖1展示了lambda表達式的語法結構以及和其對應的函數接口。

圖1 Lambda表達式的句法結構和對應的函數接口
一些研究人員發現從2014年JDK8更新至今,Java開發人員開始越來越多的使用lambda表達式。其被使用的原因主要包括了以下幾個方面:(i) 使現有代碼更加簡潔和可讀; (ii) 避免代碼重復,以及 (iii) 模擬函數的延遲計算特性等。此外,研究人員也發現有開發人員常常會低效地使用 Java 的內置函數式接口,即他們更喜歡使用通用的函數式接口而不是專用的接口,卻忽略了其可能帶來的性能開銷。類似的,我們也在開源項目中發現了越來越多由于誤用lambda表達式而引發的漏洞,缺陷,以及兼容性等問題。針對這些問題,我們開展了大量的實證研究,并取得了重要發現。
該成果“Why Do Developers Remove Lambda Expressions in Java?”已在國際頂級會議ASE'21發表。相關實驗分析數據和代碼已開源。

- 論文鏈接:
- https://ieeexplore.ieee.org/document/9678600
- 開源鏈接:
- https://github.com/CGCL-codes/LambdaMisuse
背景與動機
圖2展示了在大型開源項目Lucene中,由于lambda表達式誤用而引入的一個真實內存泄露問題。在該示例中,nextSeq是函數advanceQueue中的一個final變量,并且是此函數唯一引用的外部變量。由于此lambda表達式僅引用了一個final的外部變量,因此被稱為無狀態的lambda表達式。在運行時該lambda表達式和靜態內部類非常類似,即在JVM運行開始時加載到JVM,JVM運行結束之后該對象會被回收。因此,這個函數每次調用時都會引用該lambda表達式中的nextSeqNo變量,并且這個引用會一直維持到JVM運行結束,而不會被JVM的Garbage Collector回收,這樣就會導致內存泄漏和大量的內存開銷,甚至可能導致內存溢出異常。為了修復該問題,開發人員刪除了該lambda表達式,并將其替換成了普通的函數調用。

圖2 由lambda表達式導致的內存泄露問題及其修復
我們觀察到,在開源項目中引入 lambda 表達式后,由于其引入的各種問題,開發人員將 其修改回傳統的代碼實現是非常普遍的。然而,目前還沒有研究工作系統地研究過使用lambda表達式帶來的問題及其嚴重性,以及lambda表達式不適用的場景。為了填補這個空白,我們對大型Java開源項目中的海量被刪除的lambda表達式的進行定量分析、定性分析以及面向開發者的實證研究調查,最終發現了由于誤用lambda表達式而導致的九種常見嚴重負面影響以及一系列不適宜使用lambda表達式的復雜場景。我們的工作為未來這方面的研究指明了方向,并且能夠幫助Java開發人員更好的利用lambda表達式。
定量研究

圖3 Java開源項目中被刪除的lambda表達式數量統計
我們觀察到在Java開源項目中,lambda表達式被刪除是非常常見的現象,并且呈不斷上升的趨勢,如圖3所示。更糟糕的是,我們觀察到 lambda 表達式經常被誤用,在這種情況下,lambda 會導致缺陷漏洞或副作用,例如效率問題或內存泄漏。因此,研究開發人員不當使用和刪除的 lambda 表達式的特征引起了我們的極大興趣。我們通過對比被刪除的lambda表達式和高質量的lambda表達式的句法特征和語義特征,嘗試發現哪些lambda更可能被誤用以及刪除。具體來說,我們選取了103個大型的Apache的Java開源項目(至少1000個commit,10個committer)進行定量分析,從中提取了3662個被刪除的lambda表達式和31,228個被保留的lambda表達式。定量分析結果如圖4所示。通過對比這兩類lambda表達式的特征,我們有以下幾個主要發現:1)實現自定義函數接口的lambda表達式更容易被刪除;2)表達式主體更復雜的lambda表達式更容易被刪除;3)被自定義的函數調用的lambda表達式更容易被刪除。

圖4 被刪除lambda表達式與高質量lambda表達式的定量分析
定性研究
我們也對lambda表達式的誤用進行了定性分析。具體來說,我們從Apache JIRA,GitHub Commits和GitHub Issues等來源收集了錯誤使用lambda表達式的實例,以及其導致的缺陷或漏洞問題。通過仔細研究,我們將這些lambda表達式引入的問題總結成了以下主要幾類:
1) 性能下降。誤用lambda表達式可能導致內存泄漏,如圖2所示,lambda表達式可能在短時間內造成較大的內存開銷,導致程序性能下降。或者在頻繁調用的代碼語句中使用有狀態的lambda表達式,造成過量的對象內存分配,給Garbage Collector帶來巨大壓力,也會導致程序性能下降。
2) 較差的可讀性。Lambda表達式的引入的動機之一是解決匿名內部類的“高度問題”。比如圖5左邊所示的匿名內部類5行代碼其實只有1行發揮了作用。轉化為lambda表達式之后能用1行代碼實現相同功能。但是,我們發現一些lambda表達式可能會引入“寬度問題”,即lambda表達式過于冗長從而影響了代碼的可讀性,如圖5所示。

圖5 可轉換為lambda表達式的匿名內部類以及lambda引入的“寬度問題”
3) 序列化問題。序列化是Java語言非常重要的功能,然而序列化lambda表達式是不被推薦的。因為序列化一個合成類(編譯lambda表達式時會產生lambda表達式對應的合成類)的方式在不同的JVM可能不同,因此可能存在兼容性問題。因此,在一個JVM上序列化一個lambda表達式然后在另一個JVM上反序列化這個lambda表達式可能會帶來兼容性問題。
4) 較差的可擴展性。Java lambda表達式本質上對應一個函數接口(只含有一個抽象方法的接口)。但是在開發過程中,可能需要增加一些方法,而增加方法之后接口就不再是函數接口了,不能被lambda表達式實現,因此必須改成匿名內部類等。
5) 類型推斷失敗。Lambda表達式的類型信息有時可以省略,而由編譯器來負責推斷。但是如果下上下文信息不足,會讓編譯器推斷類型失敗,從而引發錯誤。
6) 較差的可維護性。由于lambda表達式編譯后產生一個合成類,并且合成類是沒有明確命名的(lambda表達式本身就相當于匿名類)。如果這個合成類運行時拋出異常,那么其所對應的Stack Trace可讀性就很差,如圖6所示,非常不利于程序的開發與維護。

圖6 Lambda表達式拋出異常之后的Stack Trace
7)延遲計算。Lambda表達式的一個關鍵特性就是延遲計算。延遲計算就是指在需要的時候再對表達式進行求值。然而,開發人員往往在使用時忽略了這一特性,導致實現的功能出錯或引入其他問題。
通過進一步對搜集數據的定性分析,我們總結了7種關于lambda表達式誤用的遷移模式,即開發人員是如何修復使用錯誤的lambda表達式的(具體請參見原文)。同時,我們也分析了在Java語言中lambda表達式的誤用情況(如上文總結所示)與遷移模式之間的關系,如圖7所示。最終,我們結合對lambda表達式誤用實例的理解、代碼遷移模式和開源項目中開發者的反饋,總結了一些使用lambda表達式的建議。例如不要在影響性能關鍵的代碼片段中使用lambda表達式;不要在條件分支處理的時候使用Java 8引入的API和lambda表達式;盡量在使用lambda表達式時指明變量與表達式類型;如果需要拋出檢查型異常,則不要使用lambda表達式;盡量避免使用太復雜的lambda表達式等。我們的工作不僅為未來的相關研究指明了方向,同時也能夠幫助Java開發人員更好的利用lambda表達式這一函數式特性。

圖7 Lambda表達式的誤用情況與遷移模式之間的關系
詳細內容參見:
Zheng, Mingwei, Jun Yang, Ming Wen, Hengcheng Zhu, Yepang Liu, and Hai Jin. "Why Do Developers Remove Lambda Expressions in Java?" In 2021 36th IEEE/ACM International Conference on Automated Software Engineering (ASE), pp. 67-78. IEEE, 2021.