Java Lambda表達式


    原文做者:Jakob Jenkov     原文連接:http://tutorials.jenkov.com/java/lambda-expressions.htmlhtml

@java



Java Lambda表達式是Java8中的新特性。Java lambda表達式是Java進入函數式編程的第一步。所以,Java lambda表達式是能夠單首創建的函數,而無需屬於任何類。Java lambda 表達式能夠像對象同樣傳遞並按需執行。express

Java lambda表達式一般用於實現簡單的事件監聽/回調,或在Java Streams API 函數式編程時使用。編程


Java Lambdas和函數式接口

函數式編程一般用於實現事件偵聽器。Java中的事件監聽器一般被定義爲具備一個抽象方法的Java接口。ide

這是一個模擬的單個抽象方法接口示例:函數式編程

public interface StateChangeListener {
	public void onStateChange(State oldState, State newState);
}

這個Java接口定義了一個抽象方法,只要狀態發生變化(不管觀察到什麼),都將調用該方法。函數

在Java 7中,你必須實現此接口才能監聽狀態的更改。假設你有一個名爲StateOwner的類,能夠註冊狀態監聽器。示例以下:this

public class StateOwner {
  public void addStateLister(StateChangeListener stateChangeListener) {
   //do some thing
 };
}

在Java 7中,你可使用匿名接口實現添加監聽器,以下所示:設計

StateOwner stateOwner = new StateOwner();
		stateOwner.addStateLister(new StateChangeListener() {
			@Override
			public void onStateChange(State oldState, State newState) {
				System.out.println("State changed");
			}
		});

在Java 8中你可使用Lambda表達式來添加監聽器,以下:code

StateOwner stateOwner = new StateOwner();
		stateOwner.addStateLister(
			(oldState, newState) -> System.out.println("State change")
		);

這一部分是Lambda表達式:

(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表達式實現如下接口:

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表達式實現,由於只有一個抽象方法。

實現以下:

MyInterface myInterface = (String text) -> {
    System.out.print(text);
};

Lambda表達式 vs 匿名接口實現

即便lambda表達式接近匿名接口實現,但也有一些區別須要注意。

最主要的區別,匿名接口實現能夠具備狀態(成員變量),而lambda表達式則不能。

看一下下面這個接口:

public interface MyEventConsumer {

    public void consume(Object event);

}

可使用匿名接口實現方式來實現此接口,以下所示:

MyEventConsumer consumer = new MyEventConsumer() {
    public void consume(Object event){
        System.out.println(event.toString() + " consumed");
    }
};

此匿名MyEventConsumer實現能夠具備本身的內部狀態。

重寫匿名接口實現:

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以前,在進行匿名接口實現時,必須指定要實現的接口。這是本文開頭的匿名接口實現示例:

stateOwner.addStateListener(new StateChangeListener() {

    public void onStateChange(State oldState, State newState) {
        // do something with the old and new state.
    }
});

使用lambda表達式時,一般能夠從相關的代碼中推斷出類型。例如,能夠從addStateListener()方法(StateChangeListener接口上的抽象方法)的方法聲明中推斷參數的接口類型。

這稱爲類型推斷。編譯器經過在其餘地方尋找類型來推斷參數的類型——在這種狀況下爲方法定義。這是本文開頭的示例,lambda表達式中並未聲明參數的類型:

stateOwner.addStateListener(
    (oldState, newState) -> System.out.println("State changed")
);

在lambda表達式中,一般能夠推斷參數類型。在上面的示例中,編譯器能夠從onStateChange()方法聲明中推斷其類型。所以,從onStateChange()方法的方法聲明中就能夠推斷出參數 oldState 和 newState 的類型。


Lambda參數

因爲Java lambda表達式實際上只是方法,所以lambda表達式能夠像方法同樣接受參數。前面顯示的lambda表達式的(oldState,newState)部分指定lambda表達式使用的參數。這些參數必須與函數式接口的抽象方法參數匹配。在當前這個示例,參數必須與StateChangeListener接口的onStateChange()方法的參數匹配:

public void onStateChange(State oldState, State newState);

首先,lambda表達式中的參數數量必須與方法匹配。

其次,若是你在lambda表達式中指定了任何參數類型,則這些類型也必須匹配。我尚未向你演示如何在lambda表達式參數上設置類型(本文稍後展現),可是在大多數狀況下,你不會用到它。


無參數

若是lambda表達式匹配的方法無參數,則能夠這樣寫lambda表達式:

() -> System.out.println("Zero parameter lambda");

請注意,括號中沒有內容。那就是表示lambda不帶任何參數。


一個參數

若是Java lambda表達式匹配的方法有一個參數,則能夠這樣寫lambda表達式:

(param) -> System.out.println("One parameter: " + param);

請注意,參數在括號內列出。

當lambda表達式是單個參數時,也能夠省略括號,以下所示:

param -> System.out.println("One parameter: " + param);

多個參數

若是Java lambda表達式匹配的方法有多個參數,則須要在括號內列出這些參數。代碼以下:

(p1, p2) -> System.out.println("Multiple parameters: " + p1 + ", " + p2);

僅當方法是單個參數時,才能夠省略括號。


指定參數類型

若是編譯器沒法從lambda匹配的函數式接口抽象方法推斷參數類型,則有時可能須要爲lambda表達式指定參數類型。不用擔憂,編譯器會在這種狀況下會有提醒。這是一個Java lambda指定參數類型示例:

(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關鍵字做爲參數類型的示例:

Function<String, String> toLowerCase = (var input) -> input.toLowerCase();

Lambda表達式主體

lambda表達式的主體以及它表示的函數/方法的主體在lambda聲明中的->的右側指定:

這是一個示例:

(oldState, newState) -> System.out.println("State changed")

若是你的lambda表達式須要包含多行,則能夠將lambda函數主體括在{}括號內,Java在其餘地方聲明方法時也須要將其括起來。這是一個例子:

(oldState, newState) -> {
    System.out.println("Old state: " + oldState);
    System.out.println("New state: " + newState);
  }

Lambda表達式返回值

你能夠從Java lambda表達式返回值,就像從方法中返回值同樣。你只需向lambda表達式主體添加一個return,以下所示:

(param) -> {
    System.out.println("param: " + param);
    return "return value";
  }

若是你的lambda表達式只須要計算一個返回值並將其返回,則能夠用更短的方式指定返回值。例如這個:

(a1, a2) -> { return a1 > a2; }

你能夠寫成:

(a1, a2) -> a1 > a2;

而後,編譯器會判定表達式 a1> a2 是lambda表達式的返回值。


Lambdas做爲對象

Java lambda表達式本質上是一個對象。你能夠將變量指向lambda表達式並傳遞,就像處理其餘任何對象同樣。這是一個例子:

public interface MyComparator {

    public boolean compare(int a1, int a2);

}
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主體外部聲明的局部變量的值。爲了說明這一點,首先看一下這個函數式接口:

public interface MyFactory {
    public String create(char[] chars);
}

如今,看一下實現MyFactory接口的lambda表達式:

MyFactory myFactory = (chars) -> {
    return new String(chars);
};

如今,此lambda表達式僅引用傳遞給它的參數值(chars)。可是咱們能夠改變一下。這是引用在lambda函數主體外部聲明的String變量的更新版本:

String myString = "Test";

MyFactory myFactory = (chars) -> {
    return myString + ":" + new String(chars);
};

如你所見,lambda表達式主體如今引用了在lambda表達式主體外部聲明的局部變量myString。當且僅當被引用的變量是「有效只讀(若是一個局部變量在初始化後從未被修改過,那麼它就是有效只讀)」時纔有可能,這意味着在賦值以後它不會改變其值。若是myString變量的值稍後更改,則編譯器將抱怨從lambda主體內部對其的引用。


實例變量捕獲

Lambda表達式還能夠捕獲建立Lambda的對象中的實例變量。這是示例:

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表達式主體內部引用靜態變量:

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實現提供了更簡潔的方式表示該方法調用。

首先,這是一個函數式接口:

public interface MyPrinter{
    public void print(String s);
}

如下是建立實現MyPrinter接口的Java lambda表達式的示例:

MyPrinter myPrinter = (s) -> { System.out.println(s); };

因爲lambda主體僅由一個語句組成,所以咱們實際上能夠省略括號{}。另外,因爲lambda方法只有一個參數,所以咱們能夠省略該參數周圍的括號()。更改以後的lambda表達式:

MyPrinter myPrinter = s -> System.out.println(s);

因爲全部lambda主體所作的工做都是將字符串參數轉發給System.out.println()方法,所以咱們能夠將上述lambda聲明替換爲方法引用。如下是lambda表達式引用方法的實例:

MyPrinter myPrinter = System.out::println;

注意雙冒號::。它會向Java編譯器發出信號,這是方法引用。引用的方法是雙冒號以後的內容。擁有被引用方法的任何類或對象都在雙冒號以前。

你能夠引用如下類型的方法:

  • 靜態方法
  • 參數對象的實例方法
  • 實例方法
  • 構造方法

如下各節介紹了每種類型的方法引用。


靜態方法引用

最容易引用的方法是靜態方法。

首先是函數式接口的示例:

public interface Finder {
    public int find(String s1, String s2);
}

這是一個靜態方法:

public class MyClass{
    public static int doFind(String s1, String s2){
        return s1.lastIndexOf(s2);
    }
}

最後是引用靜態方法的Java lambda表達式:

Finder finder = MyClass::doFind;

因爲Finder.find()和MyClass.doFind()方法的參數匹配,所以能夠建立實現Finder.find()並引用MyClass.doFind()方法的lambda表達式。


參數方法引用

也能夠將其中一個參數的方法引用到lambda。

函數式接口以下:

public interface Finder {
    public int find(String s1, String s2);
}

該接口用於表示能在s1中搜索s2的出現的部分。下面是一個Java lambda表達式的示例,它調用indexOf() 搜索:

Finder finder = String::indexOf;

這等價如下lambda定義:

Finder finder = (s1, s2) -> s1.indexOf(s2);

請注意簡潔方式版本是如何引用單個方法的。Java編譯器嘗試將引用的方法與第一個參數類型相匹配,使用第二個參數類型做爲被引用方法的參數。


實例方法引用

第三,還能夠從lambda表達式中引用實例方法。

首先,讓咱們來看一個函數式接口定義:

public interface Deserializer {
    public int deserialize(String v1);
}

此接口表示一個組件,該組件可以將字符串「反序列化」爲int。

如今看看這個StringConverter類:

public class StringConverter {
    public int convertToInt(String v1){
        return Integer.valueOf(v1);
    }
}

convertToInt()方法與Deserializer deserialize()方法的deserialize()方法具備相同的簽名。所以,咱們能夠建立StringConverter的實例,並從Java lambda表達式引用其convertToInt()方法,以下所示:

StringConverter stringConverter = new StringConverter();

Deserializer des = stringConverter::convertToInt;

第二行建立的lambda表達式引用在第一行建立的StringConverter實例的convertToInt方法。


構造方法引用

最後,能夠引用一個類的構造方法。你能夠經過在類名後加上:: new來完成此操做,以下所示:

MyClass::new

來看看如何在lambda表達式中引用構造方法。

函數式接口定義以下:

public interface Factory {
    public String create(char[] val);
}

此接口的create()方法與String類中某個構造函數的簽名匹配。所以,此構造函數能夠被lambda表達式用到。下面是一個示例:

Factory factory = String::new;

等同於以下lambda表達式:

Factory factory = chars -> new String(chars);


水平有限,不免錯漏,歡迎指出,或直接查看原文!

相關文章
相關標籤/搜索