《Java 8函數語言程式設計》選讀:第一個Lambda表示式
Java 8的最大變化是引入了Lambda表示式——一種緊湊的、傳遞行為的方式。它也是本書後續章節所述內容賴以依存的基礎,因此,接下來就瞭解一下什麼是Lambda表示式。
2.1 第一個Lambda表示式
Swing是一個與平臺無關的Java類庫,用來編寫圖形使用者介面(GUI)。該類庫有一個常見用法:為了響應使用者操作,需要註冊一個事件監聽器。一旦使用者輸入,監聽器會執行一些操作。(見例2-1)。
例2-1 使用匿名內部類將行為和按鈕單擊進行關聯
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
System.out.println("button clicked");
}
});
在這個例子中,我們建立了一個新物件,該物件實現了ActionListener
介面。這個介面只有一個方法:actionPerformed
,當使用者點選螢幕上的按鈕時,該方法就會被button
呼叫。匿名內部類實現了該方法。在例2-1中該方法所執行的只是列印出一條資訊,表明按鈕已被點選。
這實際上是一個程式碼即資料的例子——我們給按鈕傳遞了一個代表某種行為的物件。
設計匿名內部類的目的,就是為了方便Java程式設計師將程式碼作為資料傳遞。不過,匿名內部類還是不夠簡便。為了呼叫一行重要的邏輯程式碼,不得不加上四行冗繁的樣板程式碼。若把樣板程式碼用其他顏色區分開來,就可一目瞭然:
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
System.out.println("button clicked");
}
});
儘管如此,冗餘的樣板程式碼並不是唯一的問題:這些程式碼還相當難讀,因為它沒有清楚地表達程式設計師的意圖。我們不想傳入物件,只想傳入行為。在Java 8中,上述程式碼可以寫成一個Lambda表示式,如例2-2所示。
例2-2 使用Lambda表示式將行為和按鈕單擊進行關聯
button.addActionListener(event -> System.out.println("button clicked"));
和傳入一個實現某介面的物件不同,我們傳入了一段程式碼塊——一個沒有名字的函式。event
是引數名,和上面匿名內部類示例中的是同一個引數。->
將引數和Lambda表示式的主體分開,而主體是使用者點選按鈕時會執行的一些程式碼。
和使用匿名內部類的另一處不同在於宣告event
引數的方式。使用匿名內部類時需要顯式地宣告引數型別ActionEvent event
,而在Lambda表示式中無需指定型別,程式依然可以編譯。這是因為javac
根據程式的上下文(addActionListener
方法的簽名)在後臺推斷出了引數event
的型別。這意味著如果引數型別不言而明,則無需顯式指定。稍後會介紹型別推斷的更多細節,現在先來看看編寫Lambda表示式的各種方式。
儘管與之前相比,Lambda表示式中的引數需要很少的樣板程式碼,但是Java 8仍然是一種靜態型別語言。為了增加可讀性並遷就我們的習慣,宣告引數時也可以包括型別資訊,而且有時編譯器不一定能根據上下文推斷出引數的型別!
2.2 如何辨別Lambda表示式
Lambda表示式除了基本的形式之外,還有幾種變體,如例2-3所示。
例2-3 編寫Lambda表示式的不同形式
Runnable noArguments = () -> System.out.println("Hello World");➊
ActionListener oneArgument = event -> System.out.println("button clicked");➋
Runnable multiStatement = () -> {➌
System.out.print("Hello");
System.out.println(" World");
};
BinaryOperator<Long> add = (x, y) -> x + y;➍
BinaryOperator<Long> addExplicit = (Long x, Long y) -> x + y;➎
➊中所示的Lambda表示式不包含引數,使用空括號()表示沒有引數。該Lambda表示式實現了Runnable
介面,該介面也只有一個run
方法,沒有引數,且返回型別為void
。
➋中所示的Lambda表示式包含且只包含一個引數,可省略引數的括號,這和例2-2中的形式一樣。
Lambda表示式的主體不僅可以是一個表示式,也可以是一段程式碼塊,使用大括號({})將程式碼塊括起來,如➌所示。該程式碼塊和普通方法遵循的規則別無二致,可以用返回或丟擲異常來退出。只有一行程式碼的Lambda表示式也可使用大括號,用以明確Lambda表示式從何處開始、到哪裡結束。
Lambda表示式也可以表示包含多個引數的方法,如➍所示。這時就有必要思考怎樣去閱讀該Lambda表示式。這行程式碼並不是將兩個數字相加,而是建立了一個函式,用來計算兩個數字相加的結果。變數add
的型別是BinaryOperator<Long>
,它不是兩個數字的和,而是將兩個數字相加的那行程式碼。
到目前為止,所有Lambda表示式中的引數型別都是由編譯器推斷得出的。這當然不錯,但有時最好也可以顯式宣告引數型別,此時就需要使用小括號將引數括起來,多個引數的情況也是如此。如➎所示。
目標型別是指Lambda表示式所在上下文環境的型別。比如,將Lambda表示式賦值給一個區域性變數,或傳遞給一個方法作為引數,區域性變數或方法引數的型別就是Lambda表示式的目標型別。
上述例子還隱含了另外一層意思:Lambda表示式的型別依賴於上下文環境,是由編譯器推斷出來的。目標型別也不是一個全新的概念。如例2-4所示,Java中初始化陣列時,陣列的型別就是根據上下文推斷出來的。另一個常見的例子是null
,只有將null
賦值給一個變數,才能知道它的型別。
例2-4 等號右邊的程式碼並沒有宣告型別,系統根據上下文推斷出型別資訊
final String[] array = { "hello", "world" };
2.3 引用值,而不是變數
如果你曾使用過匿名內部類,也許遇到過需要引用它所在方法裡的變數的情況。這個時候,需要將變數宣告為final
,如例2-5所示。將變數宣告為final
,意味著不能為其重複賦值。同時也意味著在使用final
變數時,實際上是在使用賦給該變數的一個特定的值。
例2-5 匿名內部類中使用final
區域性變數
final String name = getUserName();
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
System.out.println("hi " + name);
}
});
Java 8雖然放鬆了這一限制,可以引用非final
變數,但是該變數在既成事實上必須是final
。雖然無需將變數宣告為final
,但在Lambda表示式中,也無法用作非終態變數。如果堅持用作非終態變數,編譯器就會報錯。
既成事實上的final
是指只能給該變數賦值一次。換句話說,Lambda表示式引用的是值,而不是變數。在例2-6中,name
就是一個既成事實上的final
變數。
例2-6 Lambda表示式中引用既成事實上的final
變數
String name = getUserName();
button.addActionListener(event -> System.out.println("hi " + name));
final
就像程式碼中的線路噪聲,省去之後程式碼更易讀。當然,有些情況下,顯式地使用final
程式碼更易懂。是否使用這種既成事實上的final
變數,完全取決於個人喜好。
如果你試圖給該變數多次賦值,然後在Lambda表示式中引用它,編譯器就會報錯。比如,例2-7無法通過編譯,並顯示出錯資訊:local variables referenced from a Lambda expression must be final or effectively final
{![Lambda表示式中引用的區域性變數必須是final
或既成事實上的final
變數。——譯者注]}。
例2-7 未使用既成事實上的final
變數,導致無法通過編譯
String name = getUserName();
name = formatUserName(name);
button.addActionListener(event -> System.out.println("hi " + name));
這種行為也解釋了為什麼Lambda表示式也被稱為閉包。未賦值的變數被周邊環境封閉起來,進而被繫結到一個特定的值。在眾說紛紜的計算機程式語言圈子裡,Java是否擁有真正的閉包一直備受爭議,因為在Java中只能引用既成事實上的final
變數。名字雖異,功能相同,就好比把菠蘿叫做鳳梨,其實都是同一種水果。為了避免無意義的爭論,全書將使用“Lambda表示式”一詞。無論名字如何,如前文所述,Lambda表示式都是靜態型別的。因此,接下來就分析一下Lambda表示式本身的型別:函式介面。
2.4 函式介面
函式介面是隻有一個抽象方法的介面,用作Lambda表示式的型別。
在Java裡,所有方法引數都有固定的型別。假設將數字3
作為引數傳給一個方法,則引數的型別是int
。那麼,Lambda表示式的型別又是什麼呢?
使用單一方法的介面來表示某特定方法並反覆使用,是很早就有的習慣。使用Swing編寫過使用者介面的人對這種方式都不陌生,例2-2中的用法也是如此。這裡無需再標新立異,Lambda表示式也使用同樣的技巧,並將這種介面稱為函式介面。例2-8展示了前面例子中所用的函式介面。
例2-8 ActionListener
介面:接受ActionEvent
型別的引數,返回空
public interface ActionListener extends EventListener {
public void actionPerformed(ActionEvent event);
}
ActionListener
只有一個抽象方法:actionPerformed
,被用來表示行為:接受一個引數,返回空。記住,由於actionPerformed
定義在一個介面裡,因此abstract
關鍵字不是必需的。該介面也繼承自一個不具有任何方法的父介面:EventListener
。
這就是函式介面,介面中單一方法的命名並不重要,只要方法簽名和Lambda表示式的型別匹配即可。可在函式介面中為引數起一個有意義的名字,增加程式碼易讀性,便於更透徹地理解引數的用途。
這裡的函式介面接受一個ActionEvent
型別的引數,返回空(void
),但函式介面還可有其他形式。例如,函式介面可以接受兩個引數,並返回一個值, 還可以使用泛型,這完全取決於你要幹什麼。
使用Java程式設計,總會遇到很多函式介面,但Java開發工具包(JDK)提供的一組核心函式介面會頻繁出現。
前面已講過函式介面接收的型別,也講過javac
可以根據上下文自動推斷出引數的型別,且使用者也可以手動宣告引數型別,但何時需要手動宣告呢?下面將對型別推斷作出詳盡說明。
2.5 型別推斷
某些情況下,使用者需要手動指明型別,建議大家根據自己或專案組的習慣,採用讓程式碼最便於閱讀的方法。有時候省略型別資訊可以減少干擾,更易弄清狀況;而有時卻需要型別資訊幫助理解程式碼。經驗證發現,一開始型別資訊是有用的,但隨後可以只在真正需要時才加上型別資訊。下面將介紹一些簡單的規則,來幫助確認是否需要手動宣告引數型別。
Lambda表示式中的型別推斷,實際上是Java 7就引入的目標型別推斷的擴充套件。讀者可能已經知道Java 7中的菱形操作符,它可使javac
推斷出泛型引數的型別。參見例2-9。
例2-9 使用菱形操作符,根據變數型別做推斷
Map<String, Integer> oldWordCounts = new HashMap<String, Integer>();➊
Map<String, Integer> diamondWordCounts = new HashMap<>();➋
我們為變數oldWordCounts
➊明確指定了泛型的型別,而變數diamondWordCounts
➋則使用了菱形操作符。不用明確宣告泛型型別,編譯器就可以自己推斷出來,這就是它的神奇之處!
當然,這並不是什麼魔法,根據變數diamondWordCounts
➋的型別可以推斷出HashMap
的泛型型別,但使用者仍需要宣告變數的泛型型別。
如果將建構函式直接傳遞給一個方法,也可根據方法簽名來推斷型別。在例2-10中,我們傳入了HashMap
,根據方法簽名已經可以推斷出泛型的型別。
例2-10 使用菱形操作符,根據方法簽名做推斷
useHashmap(new HashMap<>());
...
private void useHashmap(Map<String, String> values);
Java 7中程式設計師可省略建構函式的泛型型別,Java 8更進一步,程式設計師可省略Lambda表示式中的所有引數型別。再強調一次,這並不神奇,javac
根據Lambda表示式上下文資訊就能推斷出引數的正確型別。程式依然要經過型別檢查來保證執行的安全性,但不用再明確宣告型別罷了。這就是所謂的型別推斷。
Java 8中對型別推斷系統的改善值得一提。上面的例子將new HashMap<>()
傳給useHashmap
方法,即使編譯器擁有足夠的資訊,也無法在Java 7中通過編譯。
接下來將通過舉例來詳細分析型別推斷。
例2-11和例2-12都將變數賦給一個函式介面,這樣便於理解。第一個例子(例2-11)使用Lambda表示式檢測一個Integer
是否大於5
。這實際上是一個Predicate
——用來判斷真假的函式介面。
例2-11 型別推斷
Predicate<Integer> atLeast5 = x -> x > 5;
Predicate
也是一個Lambda表示式,和前文中ActionListener
不同的是,它還返回一個值。在例2-11中,表示式x > 5
是Lambda表示式的主體。這樣的情況下,返回值就是Lambda表示式主體的值。
例2-12 Predicate
介面的原始碼,接受一個物件,返回一個布林值
public interface Predicate<T> {
boolean test(T t);
}
從例2-12中可以看出,Predicate
只有一個泛型型別的引數,Integer
用於其中。Lambda表示式實現了Predicate
介面,因此它的單一引數被推斷為Integer
型別。javac
還可檢查Lambda表示式的返回值是不是boolean
,這正是Predicate
方法的返回型別。
例2-13是一個略顯複雜的函式介面:BinaryOperator
。該介面接受兩個引數,返回一個值,引數和值的型別均相同。例項中所用的型別是Long
。
例2-13 略顯複雜的的型別推斷
BinaryOperator<Long> addLongs = (x, y) -> x + y;
型別推斷系統相當智慧,但若資訊不夠,型別推斷系統也無能為力。型別系統不漫無邊際地瞎猜,而會中止操作並報告編譯錯誤,尋求幫助。比如,刪掉例2-13中的某些型別資訊,則如例2-14所示。
例2-14 沒有泛型,程式碼則通不過編譯
BinaryOperator add = (x, y) -> x + y;
編譯器給出的報錯資訊如下:
Operator '& #x002B;' cannot be applied to java.lang.Object, java.lang.Object.
報錯資訊讓人一頭霧水,到底怎麼回事?BinaryOperator
畢竟是一個具有泛型引數的函式介面,該型別既是引數x
和y
的型別,也是返回值的型別。上面的例子中並沒有給出變數add
的任何泛型資訊,給出的正是原始型別的定義。因此,編譯器認為引數和返回值都是java.lang.Object
例項。
後文中討論“過載解析”時還會講到型別推斷,但就目前來說,掌握以上型別推斷的知識就已經足夠了。
2.6 要點回顧
- Lambda表示式是一個匿名方法,將行為像資料一樣進行傳遞。
- Lambda表示式的常見結構:
BinaryOperator<Integer> add = (x, y) → x + y
。 - 函式介面指僅具有單一抽象方法的介面,用來表示Lambda表示式的型別。
相關文章
- Java 函數語言程式設計(二)Lambda表示式Java函數程式設計
- 函數語言程式設計:Lambda 表示式函數程式設計
- Java中的函數語言程式設計(三)lambda表示式Java函數程式設計
- Lambda表示式入門--函數語言程式設計與函式式介面函數程式設計函式
- 《Java 8函數語言程式設計》選讀:為什麼要給Java 8中加入函數語言程式設計?Java函數程式設計
- Java8的函數語言程式設計Java函數程式設計
- [轉]Java 8 的 lambda 表示式 Java 8 的 lambda 表示式Java
- 《Java8函數語言程式設計》讀書筆記---類庫Java函數程式設計筆記
- 重識Java8函數語言程式設計Java函數程式設計
- Java8 新特性 —— 函數語言程式設計Java函數程式設計
- Java8函數語言程式設計應用Java函數程式設計
- 書推薦《Java 8函數語言程式設計》Java函數程式設計
- 淺談Java 8的函數語言程式設計Java函數程式設計
- Java 函數語言程式設計Java函數程式設計
- 函數語言程式設計(Lambda、Stream流、Optional等)函數程式設計
- python函數語言程式設計之yield表示式形式Python函數程式設計
- Java 8 Lambda 表示式Java
- java 8 lambda表示式Java
- Java 8 lambda 表示式10個示例Java
- 《Java8函數語言程式設計》讀書筆記---收集器Java函數程式設計筆記
- java8函數語言程式設計筆記-破壞式更新和函式式更新Java函數程式設計筆記函式
- 隨便聊聊 Java 8 的函數語言程式設計Java函數程式設計
- 使用 Java 8 函數語言程式設計生成字母序列Java函數程式設計
- 《Java8函數語言程式設計》讀書筆記---常用的流操作Java函數程式設計筆記
- Scala函式與函數語言程式設計函式函數程式設計
- 快速掌握Java8 Stream函數語言程式設計技巧Java函數程式設計
- 函數語言程式設計-鏈式程式設計RAC函數程式設計
- Java8-Lambda表示式Java
- java8 lambda表示式Java
- 掌握 Java 8 Lambda 表示式Java
- 函式式思維和函數語言程式設計函式函數程式設計
- Java中的函數語言程式設計(二)函式式介面Functional InterfaceJava函數程式設計函式Function
- 好程式設計師分享java8新特性之Lambda表示式程式設計師Java
- 倉頡程式語言技術指南:巢狀函式、Lambda 表示式、閉包巢狀函式
- 從五大語言看函式和lambda表示式函式
- 函數語言程式設計(2) 高階函式函數程式設計函式
- java8函數語言程式設計筆記-科裡化Java函數程式設計筆記
- java8函數語言程式設計筆記-延遲性Java函數程式設計筆記