探索Java語言與JVM中的Lambda表示式

發表於2013-01-04

英文原文:Java Magazine,編譯:jiangshapub

Lambda表示式是自Java SE 5引入泛型以來最重大的Java語言新特性,本文是2012年度最後一期Java Magazine中的一篇文章,它介紹了Lamdba的設計初衷,應用場景與基本語法。(2013.01.02最後更新)

Lambda表示式,這個名字由該專案的專家組選定,描述了一種新的函數語言程式設計結構,這個即將出現在Java SE 8中的新特性正被大家急切地等待著。有時你也會聽到人們使用諸如閉包,函式直接量,匿名函式,及SAM(Single Abstract Method)這樣的術語。其中一些術語彼此之間會有一些細微的不同,但基本上它們都指代相同的功能。

雖然一開始會覺得Lambda表示式看起來很陌生,但很容易就能掌握它。而且為了編寫可完全利用現代多核CPU的應用程式,掌握Lambda表示式是至關重要的。

需要牢記的一個關鍵概念就是,Lambda表示式是一個很小且能被當作資料進行傳遞的函式。需要掌握的第二個概念就是,理解集合物件是如何在內部進行遍歷的,這種遍歷不同於當前已有的外部順序化遍歷。

探索Java語言與JVM中的Lambda表示式

在本文中,我們將向你展示Lambda表示式背後的動因,應用示例,當然,還有它的語法。

為什麼你需要Lambda表示式

程式設計師需要Lambda表示式的原因主要有三個:

1. 更緊湊的程式碼

2. 通過提供額外的功能對方法的功能進行修改的能力

3. 更好地支援多核處理

更緊湊的程式碼

Lambda表示式以一種簡潔的方式去實現僅有一個方法的Java類。

例如,如果程式碼中有大量的匿名內部類–諸如用於UI應用中的監聽器與處理器實現,以及用於併發應用中的Callable與Runnable實現–在使用了Lambda表示式之後,將使程式碼變得非常短,且更易於理解。

修改方法的能力

有時,方法不具備我們想要的一些功能。例如,Collection介面中的contains()方法只有當傳入的物件確實存在於該集合物件中時才會返回true。但我們無法去幹預該方法的功能,比如,若使用不同的大小寫方案也可以認為正在查詢的字串存在於這個集合物件中,我們希望此時contains()方法也能返回true。

簡單點兒說,我們所期望做的就是”將我們自己的新程式碼傳入”已有的方法中,然後再呼叫這個傳進去的程式碼。Lambda表示式提供了一種很好的途徑來代表這種被傳入已有方法且應該還會被回撥的程式碼。

更好地支援多核處理

當今的CPU具備多個核心。這就意味著,多執行緒程式能夠真正地被並行執行,這完全不同於在單核CPU中使用時間共享這種方式。通過在Java中支援函數語言程式設計語法,Lambda表示式能幫助你編寫簡單的程式碼去高效地應用這些CPU核心。
例如,你能夠並行地操控大集合物件,通過利用並行程式設計模式,如過濾、對映和化簡(後面將會很快接觸到這些模式),就可使用到CPU中所有可用的硬體執行緒。

Lambda表示式概覽

在前面提到的使用不同大小寫方案查詢字串的例子中,我們想做的就是把方法toLowerCase()的表示法作為第二個引數傳入到contains()方法中,為此需要做如下的工作:

1. 找到一種途徑,可將程式碼片斷當作一個值(某種物件)進行處理

2. 找到一種途徑,將上述程式碼片斷傳遞給一個變數

換言之,我們需要將一個程式邏輯包裝到某個物件中,並且該物件可以被進行傳遞。為了說的更具體點兒,讓我們來看兩個基本的Lambda表示式的例子,它們都是可以被現有的Java程式碼進行替換的。

過濾

你可能想傳遞的程式碼片斷可能就是過濾器了,這是一個很好的示例。例如,假設你正在使用(Java SE 7預覽版中的)java.io.FileFilter去確定目錄隸屬於給定的路徑,如清單1所示,

清單1

在使用Lambda表示式之後,程式碼會得到極大的簡化,如清單2所示,

清單2

賦值表示式的左邊會推匯出型別(FileFilter),右邊則看起來像FileFilter介面中accept()方法的一個縮小版,該方法會接受一個File物件,在判定f.isDirectory()之後返回一個布林值。

實際上,由於Lambda表示式利用了型別推導,基於後面的工作原理,我們還可以進一步簡化上述程式碼。編譯器知道FileFilter只有唯一的方法accept(),所以它必定是該方法的實現。我們還知,accept()方法只需要一個File型別的引數。因此,f必定是File型別的。如清單3所示,

清單3

你可以看到,使用Lambda表示式會大幅降低模板程式碼的數量。

一旦你習慣於使用Lambda表示式,它會使邏輯流程變得非常易於閱讀。在達到這一目的的關鍵方法之一就是將過濾邏輯置於使用該邏輯的方法的側邊。

事件處理器

UI程式是另一個大量使用匿名內部類的領域。讓我們將一個點選監聽器賦給一個按鈕,如清單4所示,

清單4

這多麼程式碼無非是說”當點選該按鈕時,呼叫該方法”。使用Lambda表示式就可寫出如清單5所示的程式碼,

清單5

該監聽器在必要時可被複用,但如果它僅需被使用一次,清單6中的程式碼則考慮了一種很好的方式。

清單6

在這個例子中,這種使用額外花括號的語法有些古怪,但這是必須的,因為actionPerformed()方法返回的是void。後面我們會看到與此有關的更多內容。

現在讓我們轉而關注Lambda表示式在編寫處理集合物件的新式程式碼中所扮演的角色,尤其是當針對兩種程式設計風格,外部遍歷與內部遍歷,之間的轉換的時候。

外部遍歷 vs. 內部遍歷

到目前為止,處理Java集合物件的標準方式是通過外部遍歷。之所以稱其為外部遍歷,是因為要使用集合物件外部的控制流程去遍歷集合所包含的元素。這種傳統的處理集合的方式為多數Java程式設計師所熟知,儘管他們並不知道或不使用外部遍歷這個術語。

如清單7所示,Java語言為增強的for迴圈構造了一個外部迭代器,並使用這個迭代器去遍歷集合物件,

清單7

使用這種方法,集合類代表著全部元素的一個”整體”檢視,並且該集合物件還能支援對任意元素的隨機訪問,程式設計師可能會有這種需求。

基於這種觀點,可通過呼叫iterator()方法去遍歷集合物件,該方法將返回集合元素型別的迭代器,該迭代器是針對同一集合物件的更具限制性的檢視。它沒有為隨機訪問暴露任何介面;相反,它純粹是為了順序地訪問集合元素而設計的。這種順序本性使得當你試圖併發地訪問集合物件時就會造成臭名昭著的ConcurrentModificationException。

另一種可選的方案就是要求集合物件要能夠在內部管理迭代器(或迴圈),這種方案就是內部遍歷,當使用Lambda表示式時會優先選擇內部遍歷。

除了新的Lambda表示式語法以外,Lambda專案還包括一個經過大幅升級的集合框架類庫。這次升級的目的是為了能更易於編寫使用內部遍歷的程式碼,以支援一系列眾所周知的函數語言程式設計典範。

使用Lambda的函數語言程式設計

曾經,大多數開發者發現他們需要集合能夠執行如下一種或幾種操作:

1. 建立一個新的集合物件,但要過濾掉不符合條件的元素。

2. 對集合中的元素逐一進行轉化,並使用轉化後的集合。

3. 建立集合中所有元素的某個屬性的總體值,例如,合計值與平均值。這樣的任務(分別稱之為過濾,對映和化簡)具有共通的要點:它們都需要處理集合中的每個元素。

程式無論是判定某個元素是否存在,或是判斷元素是否符合某個條件(過濾),或是將元素轉化成新元素並生成新集合(對映),或是計算總體值(化簡),關鍵原理就是”程式必須處理到集合中的每個元素”。
這就暗示我們需要一種簡單的途徑去表示用於內部遍歷的程式。幸運地是,Java SE 8為此類表示法提供了構建語句塊。

支援基本函數語言程式設計的Java SE 8類

Java SE 8中的一些類意在被用於實現前述的函式式典範,這些類包括Predicate,Mapper和Block–當然,還有其它的一些類–它們都在一個新的java.util.functions包中。

看看Predicate類的更多細節,該類常被用於實現過濾演算法;將它作用於一個集合,以返回一個包含有符合謂語條件元素的新集合。何為謂語,有很多種解釋。Java SE 8認為謂語是一個依據其變數的值來判定真或假的方法。

再考慮一下我們之前看過的一個例子。給定一個字串的集合,我們想判定它是否包含有指定的字串,但希望字串的比較是大小寫不敏感的。

在Java SE 7中,我們將需要使用外部遍歷,其程式碼將如清單8所示,

清單8

而在即將釋出的Java SE 8中,我們使用Predicate以及Collections類中一個新的助手方法(過濾器)就可寫出更為緊湊的程式,如清單9所示,

清單9

事實上,如果使用更為通用的函數語言程式設計風格,你只需要寫一行程式碼,如清單10所示,

清單10

如你所見,程式碼依然非常的易讀,並且我們也體會到了使用內部遍歷的好處。

最後,讓我們討論一下Lambda表示式語法的更多細節。

Lambda表示式的語法規則

Lambda表示式的基本格式是以一個可被接受的引數列表開頭,以一些程式碼(稱之為表示式體/body)結尾,並以箭頭(->)將前兩者分隔開。

注意:Lambda表示式的語法仍可能會面臨改變,但在撰寫本文的時候,下面示例中所展示的語法是能夠正常工作的。

Lambda表示式非常倚重型別推導,與Java的其它語法相比,這顯得極其不同尋常。
讓我們進一步考慮之前已經看過的一個示例(請見清單11)。如果看看ActionListener的定義,可以發現它只有一個方法(請見清單12)。

清單11

清單12

所以,在清單11右側的Lambda表示式,能夠很容易地理解為”這是針對僅宣告單個方法的介面的方法定義”。注意,仍然必須要遵守Java靜態型別的一般規則;這是使型別推導能正確工作的唯一途徑。

據此可以發現,使用Lambda表示式可以將先前所寫的匿名內部類程式碼轉換更緊湊的程式碼。

還需要意識到有另一個怪異的語法。讓我們再回顧下上述示例,如清單13所示,

清單13

僅一瞥之,它看起來與ActionListener的示例相似,但讓我們看看FileFilter介面的定義(請見清單14)。accept()方法會返回一個布林值,但並沒有一個顯式的返回語句。相反,該返回值的型別是從Lambda表示式中推匯出來的

清單14

這就能解釋,當方法返回型別為void時,為什麼要進行特別處理了。對於這種情形,Lambda表示式會使用一對額外的小括號去包住程式碼部分(表示式體/body)。若沒有這種怪異的語法,型別推導將無法正常工作–但你要明白,這一語法可能會被改變。

Lambda表示式的表示式體可以包含多條語句,對於這種情形,表示式體需要被小括號包圍住,但”被推匯出的返回型別”這種語法將不啟作用,那麼返回型別關鍵字就必不可少。

最後還需要提醒你的是:當前,IDE似乎還不支援Lambda語法,所以當你第一次嘗試Lambda表示式時,必須要格外注意javac編譯器丟擲的任何警告。

結論

Lambda表示式是自Java SE 5引入泛型以來最重大的Java語言新特性。應用得當,Lambda表示式可使你寫出簡潔的程式碼,為已有方法增加額外的功能,並能更好地適應多核處理器。到目前為止,我們能肯定的是,你正急切地想去嘗試Lambda表示式,所以我們也別囉嗦了…

你可以從Lambda專案的主頁中獲得包含有Lambda表示式的Java SE 8快照版。同樣地,在試用二進位制包時,你也應該先閱讀一下”Lambda專案狀態”的相關文章,可以在此處找到它們。

相關文章