1. 程式人生 > >Java Lambda表示式

Java Lambda表示式


> 譯     原文作者:Jakob Jenkov     原文連結:http://tutorials.jenkov.com/java/lambda-expressions.html @[Toc]

Java Lambda表示式是Java8中的新特性。Java lambda表示式是Java進入函數語言程式設計的第一步。因此,Java lambda表示式是可以單獨建立的函式,而無需屬於任何類。Java lambda 表示式可以像物件一樣傳遞並按需執行。 Java lambda表示式通常用於實現簡單的事件監聽/回撥,或在Java Streams API 函數語言程式設計時使用。
# Java Lambdas和函式式介面 函數語言程式設計通常用於實現事件偵聽器。Java中的事件監聽器通常被定義為具有一個抽象方法的Java介面。 這是一個模擬的單個抽象方法介面示例: ```java public interface StateChangeListener { public void onStateChange(State oldState, State newState); } ``` 這個Java介面定義了一個抽象方法,只要狀態發生變化(無論觀察到什麼),都將呼叫該方法。 在Java 7中,你必須實現此接口才能監聽狀態的更改。假設你有一個名為StateOwner的類,可以註冊狀態監聽器。示例如下: ```java public class StateOwner { public void addStateLister(StateChangeListener stateChangeListener) { //do some thing }; } ``` 在Java 7中,你可以使用匿名介面實現新增監聽器,如下所示: ```java StateOwner stateOwner = new StateOwner(); stateOwner.addStateLister(new StateChangeListener() { @Override public void onStateChange(State oldState, State newState) { System.out.println("State changed"); } }); ``` 在Java 8中你可以使用Lambda表示式來新增監聽器,如下: ```java StateOwner stateOwner = new StateOwner(); stateOwner.addStateLister( (oldState, newState) -> System.out.println("State change") ); ``` 這一部分是Lambda表示式: ```java (oldState, newState) -> System.out.println("State changed") ``` lambda表示式與addStateListener()方法的引數的引數型別匹配。如果lambda表示式與引數型別(在本例中為StateChangeListener介面)匹配,則將lambda表示式轉換為實現與該引數相同的介面的函式。 Java lambda表示式只能在它們匹配的型別是單個方法介面的地方使用。 在上面的示例中,lambda表示式作為引數,其中引數型別為StateChangeListener介面。該介面只有一個抽象方法。因此,lambda表示式成功匹配該介面。
## 將Lambda匹配到介面 單個抽象方法介面有時也稱為函式式介面。
將Java lambda表示式與函式式介面進行匹配需要以下步驟: * 介面是否只有一個抽象方法? * lambda表示式的引數是否與抽象方法的引數匹配? * lambda表示式的返回型別是否與抽象方法的返回型別匹配? 如果這三個條件都滿足,則該介面可以匹配給定的lambda表示式。
## 具有預設方法和靜態方法的介面 從Java 8開始,Java介面可以包含預設方法和靜態方法。預設方法和靜態方法都可以在介面中直接實現。這意味著,Java lambda表示式可以使用多種方法實現介面——只要該介面僅有一個抽象方法即可。 可以使用lambda表示式實現以下介面: ```java import java.io.IOException; import java.io.OutputStream; public interface MyInterface { void printIt(String text); default public void printUtf8To(String text, OutputStream outputStream){ try { outputStream.write(text.getBytes("UTF-8")); } catch (IOException e) { throw new RuntimeException("Error writing String as UTF-8 to OutputStream", e); } } static void printItToSystemOut(String text){ System.out.println(text); } } ``` 即使此介面包含3個方法,也可以通過lambda表示式實現,因為只有一個抽象方法。 實現如下: ```java MyInterface myInterface = (String text) -> { System.out.print(text); }; ```
# Lambda表示式 vs 匿名介面實現 即使lambda表示式接近匿名介面實現,但也有一些區別需要注意。 最主要的區別,匿名介面實現可以具有狀態(成員變數),而lambda表示式則不能。 看一下下面這個介面: ```java public interface MyEventConsumer { public void consume(Object event); } ``` 可以使用匿名介面實現方式來實現此介面,如下所示: ```java MyEventConsumer consumer = new MyEventConsumer() { public void consume(Object event){ System.out.println(event.toString() + " consumed"); } }; ``` 此匿名MyEventConsumer實現可以具有自己的內部狀態。 重寫匿名介面實現: ```java MyEventConsumer myEventConsumer = new MyEventConsumer() { private int eventCount = 0; public void consume(Object event) { System.out.println(event.toString() + " consumed " + this.eventCount++ + " times."); } }; ``` 請注意,匿名MyEventConsumer介面實現現在具有一個名為eventCount的屬性。 Lambda表示式不能具有此類屬性。因此,lambda表示式是無狀態的。
# Lambda型別推斷 在Java 8之前,在進行匿名介面實現時,必須指定要實現的介面。這是本文開頭的匿名介面實現示例: ```java stateOwner.addStateListener(new StateChangeListener() { public void onStateChange(State oldState, State newState) { // do something with the old and new state. } }); ``` 使用lambda表示式時,通常可以從相關的程式碼中推斷出型別。例如,可以從addStateListener()方法(StateChangeListener介面上的抽象方法)的方法宣告中推斷引數的介面型別。 這稱為型別推斷。編譯器通過在其他地方尋找型別來推斷引數的型別——在這種情況下為方法定義。這是本文開頭的示例,lambda表示式中並未宣告引數的型別: ```java stateOwner.addStateListener( (oldState, newState) -> System.out.println("State changed") ); ``` 在lambda表示式中,通常可以推斷引數型別。在上面的示例中,編譯器可以從onStateChange()方法宣告中推斷其型別。因此,從onStateChange()方法的方法宣告中就可以推斷出引數 oldState 和 newState 的型別。
# Lambda引數 由於Java lambda表示式實際上只是方法,因此lambda表示式可以像方法一樣接受引數。前面顯示的lambda表示式的(oldState,newState)部分指定lambda表示式使用的引數。這些引數必須與函式式介面的抽象方法引數匹配。在當前這個示例,引數必須與StateChangeListener介面的onStateChange()方法的引數匹配: ```java public void onStateChange(State oldState, State newState); ``` 首先,lambda表示式中的引數數量必須與方法匹配。 其次,如果你在lambda表示式中指定了任何引數型別,則這些型別也必須匹配。我還沒有向你演示如何在lambda表示式引數上設定型別(本文稍後展示),但是在大多數情況下,你不會用到它。
## 無引數 如果lambda表示式匹配的方法無引數,則可以這樣寫lambda表示式: ```java () -> System.out.println("Zero parameter lambda"); ``` 請注意,括號中沒有內容。那就是表示lambda不帶任何引數。
## 一個引數 如果Java lambda表示式匹配的方法有一個引數,則可以這樣寫lambda表示式: ```java (param) -> System.out.println("One parameter: " + param); ``` 請注意,引數在括號內列出。 當lambda表示式是單個引數時,也可以省略括號,如下所示: ```java param -> System.out.println("One parameter: " + param); ```
## 多個引數 如果Java lambda表示式匹配的方法有多個引數,則需要在括號內列出這些引數。程式碼如下: ```java (p1, p2) -> System.out.println("Multiple parameters: " + p1 + ", " + p2); ``` 僅當方法是單個引數時,才可以省略括號。
## 指定引數型別 如果編譯器無法從lambda匹配的函式式介面抽象方法推斷引數型別,則有時可能需要為lambda表示式指定引數型別。不用擔心,編譯器會在這種情況下會有提醒。這是一個Java lambda指定引數型別示例: ```java (Car car) -> System.out.println("The car is: " + car.getName()); ``` 如你所見,car引數的型別(Car)寫在引數名稱的前面,就像在其他方法中宣告引數或對介面進行匿名實現時一樣。
## Java 11中的var引數型別 在Java 11中,你可以使用var關鍵字作為引數型別。 var關鍵字在Java 10中作為區域性變數型別推斷引入。從Java 11開始,var也可以用於lambda引數型別。這是在lambda表示式中使用Java var關鍵字作為引數型別的示例: ```java Function toLowerCase = (var input) -> input.toLowerCase(); ```
# Lambda表示式主體 lambda表示式的主體以及它表示的函式/方法的主體在lambda宣告中的->的右側指定: 這是一個示例: ```java (oldState, newState) -> System.out.println("State changed") ``` 如果你的lambda表示式需要包含多行,則可以將lambda函式主體括在{}括號內,Java在其他地方宣告方法時也需要將其括起來。這是一個例子: ```java (oldState, newState) -> { System.out.println("Old state: " + oldState); System.out.println("New state: " + newState); } ```
# Lambda表示式返回值 你可以從Java lambda表示式返回值,就像從方法中返回值一樣。你只需向lambda表示式主體新增一個return,如下所示: ```java (param) -> { System.out.println("param: " + param); return "return value"; } ``` 如果你的lambda表示式只需要計算一個返回值並將其返回,則可以用更短的方式指定返回值。例如這個: ```java (a1, a2) -> { return a1 > a2; } ``` 你可以寫成: ```java (a1, a2) -> a1 > a2; ``` 然後,編譯器會斷定表示式 a1> a2 是lambda表示式的返回值。
# Lambdas作為物件 Java lambda表示式本質上是一個物件。你可以將變數指向lambda表示式並傳遞,就像處理其他任何物件一樣。這是一個例子: ```java public interface MyComparator { public boolean compare(int a1, int a2); } ``` ```java MyComparator myComparator = (a1, a2) -> return a1 > a2; boolean result = myComparator.compare(2, 5); ``` 第一個程式碼塊顯示了lambda表示式實現的介面。 第二個程式碼塊顯示了lambda表示式的定義,lambda表示式如何分配給變數,以及最後如何通過呼叫其實現的介面方法來呼叫lambda表示式。
# 變數捕獲 在某些情況下,Java lambda表示式能夠訪問在lambda表示式主體外部宣告的變數。 Java lambdas可以捕獲以下型別的變數: * 區域性變數 * 例項變數 * 靜態變數 這些變數捕獲的每一個將在以下各節中進行描述。
## 區域性變數捕獲 Java lambda可以捕獲在lambda主體外部宣告的區域性變數的值。為了說明這一點,首先看一下這個函式式介面: ```java public interface MyFactory { public String create(char[] chars); } ``` 現在,看一下實現MyFactory介面的lambda表示式: ```java MyFactory myFactory = (chars) -> { return new String(chars); }; ``` 現在,此lambda表示式僅引用傳遞給它的引數值(chars)。但是我們可以改變一下。這是引用在lambda函式主體外部宣告的String變數的更新版本: ```java String myString = "Test"; MyFactory myFactory = (chars) -> { return myString + ":" + new String(chars); }; ``` 如你所見,lambda表示式主體現在引用了在lambda表示式主體外部宣告的區域性變數myString。當且僅當被引用的變數是“有效只讀(如果一個區域性變數在初始化後從未被修改過,那麼它就是有效只讀)”時才有可能,這意味著在賦值之後它不會改變其值。如果myString變數的值稍後更改,則編譯器將抱怨從lambda主體內部對其的引用。
## 例項變數捕獲 Lambda表示式還可以捕獲建立Lambda的物件中的例項變數。這是示例: ```java public class EventConsumerImpl { private String name = "MyConsumer"; public void attach(MyEventProducer eventProducer){ eventProducer.listen(e -> { System.out.println(this.name); }); } } ``` 注意lambda表示式主體中對this.name的引用。這將捕獲封閉的EventConsumerImpl物件的 name 例項變數。甚至可以在捕獲例項變數後更改其值——該值將反映在lambda內部。 this
語義實際上是Java lambda與介面的匿名實現不同的地方之一。匿名介面實現可以有自己的例項變數,這些例項變數可以通過this進行引用。但是,lambda不能擁有自己的例項變數,因此它始終指向封閉的物件。 注意:EventConsumer的設計不是很優雅。我只是這樣寫來說明例項變數捕獲。
## 靜態變數捕獲 Java lambda表示式還可以捕獲靜態變數。 因為只要可以訪問靜態變數(包作用域或public作用域),Java應用程式中的任何地方都可以訪問靜態變數。 這是一個建立lambda表示式的示例類,該lambda表示式從lambda表示式主體內部引用靜態變數: ```java public class EventConsumerImpl { private static String someStaticVar = "Some text"; public void attach(MyEventProducer eventProducer){ eventProducer.listen(e -> { System.out.println(someStaticVar); }); } } ``` lambda捕獲到靜態變數後,它的值也可以更改。 同樣,上述類設計不太合理。不要對此考慮太多。該類主要用於向你顯示lambda表示式可以訪問靜態變數。
# Lambda方法引用 如果你的lambda表示式所做的只是用傳遞給lambda的引數呼叫另一個方法,則Java lambda實現提供了更簡潔的方式表示該方法呼叫。 首先,這是一個函式式介面: ```java public interface MyPrinter{ public void print(String s); } ``` 以下是建立實現MyPrinter介面的Java lambda表示式的示例: ```java MyPrinter myPrinter = (s) -> { System.out.println(s); }; ``` 由於lambda主體僅由一個語句組成,因此我們實際上可以省略括號{}。另外,由於lambda方法只有一個引數,因此我們可以省略該引數周圍的括號()。更改之後的lambda表示式: ```java MyPrinter myPrinter = s -> System.out.println(s); ``` 由於所有lambda主體所做的工作都是將字串引數轉發給System.out.println()方法,因此我們可以將上述lambda宣告替換為方法引用。以下是lambda表示式引用方法的例項: ```java MyPrinter myPrinter = System.out::println; ``` 注意雙冒號::。它會向Java編譯器發出訊號,這是方法引用。引用的方法是雙冒號之後的內容。擁有被引用方法的任何類或物件都在雙冒號之前。 你可以引用以下型別的方法: * 靜態方法 * 引數物件的例項方法 * 例項方法 * 構造方法 以下各節介紹了每種型別的方法引用。
## 靜態方法引用 最容易引用的方法是靜態方法。 首先是函式式介面的示例: ```java public interface Finder { public int find(String s1, String s2); } ``` 這是一個靜態方法: ```java public class MyClass{ public static int doFind(String s1, String s2){ return s1.lastIndexOf(s2); } } ``` 最後是引用靜態方法的Java lambda表示式: ```java Finder finder = MyClass::doFind; ``` 由於Finder.find()和MyClass.doFind()方法的引數匹配,因此可以建立實現Finder.find()並引用MyClass.doFind()方法的lambda表示式。
## 引數方法引用 也可以將其中一個引數的方法引用到lambda。 函式式介面如下: ```java public interface Finder { public int find(String s1, String s2); } ``` 該介面用於表示能在s1中搜索s2的出現的部分。下面是一個Java lambda表示式的示例,它呼叫indexOf() 搜尋: ```java Finder finder = String::indexOf; ``` 這等價以下lambda定義: ```java Finder finder = (s1, s2) -> s1.indexOf(s2); ``` 請注意簡潔方式版本是如何引用單個方法的。Java編譯器嘗試將引用的方法與第一個引數型別相匹配,使用第二個引數型別作為被引用方法的引數。
## 例項方法引用 第三,還可以從lambda表示式中引用例項方法。 首先,讓我們來看一個函式式介面定義: ```java public interface Deserializer { public int deserialize(String v1); } ``` 此介面表示一個元件,該元件能夠將字串“反序列化”為int。 現在看看這個StringConverter類: ```java public class StringConverter { public int convertToInt(String v1){ return Integer.valueOf(v1); } } ``` convertToInt()方法與Deserializer deserialize()方法的deserialize()方法具有相同的簽名。因此,我們可以建立StringConverter的例項,並從Java lambda表示式引用其convertToInt()方法,如下所示: ```java StringConverter stringConverter = new StringConverter(); Deserializer des = stringConverter::convertToInt; ``` 第二行建立的lambda表示式引用在第一行建立的StringConverter例項的convertToInt方法。
## 構造方法引用 最後,可以引用一個類的構造方法。你可以通過在類名後加上:: new來完成此操作,如下所示: ```java MyClass::new ``` 來看看如何在lambda表示式中引用構造方法。 函式式介面定義如下: ```java public interface Factory { public String create(char[] val); } ``` 此介面的create()方法與String類中某個建構函式的簽名匹配。因此,此建構函式可以被lambda表示式用到。下面是一個示例: ```java Factory factory = String::new; ``` 等同於如下lambda表示式: ```java Factory factory = chars -> new String(chars); ```

水平有限,難免錯漏,歡迎指出,或直接檢視