Java Lambda Expressions

weixin_34075551發表於2018-11-22

java 萊姆達表示式是java8的新特性,java lambda表示式是java的邁向函數語言程式設計的第一步。因此,java的lambda表示式其實是一個方法,它可以不從屬於任何一個類。java表示式可以作為一個引數,就好像它是一個物件一樣。它可以在需求時被執行。

java的lambda表示式通常被用於實現簡單的事件監聽器或回撥函式,或者和javaStreamApi一起實現函數語言程式設計。

1.java Lambda 以及單一方法介面

函數語言程式設計通常被用於實現事件監聽器。java中的事件監聽器通常被定義為含有單一方法的介面,這裡有一個虛構的單一方法介面:

public interface StateChangeListener {

    public void onStateChange(State oldState, State newState);

}

這個java介面定義了一個單一的方法,當狀態改變時,會呼叫這個方法。

在java7中,為了監聽狀態的改變,你就不得不實現這個介面,想象一下,你有一個類叫: StateOwner,它可以註冊狀態事件監聽器,這是一個例子:

public class StateOwner {

    public void addStateListener(StateChangeListener listener) { ... }

}

在java7中,你可以使用匿名內部類去新增一個事件監聽器,就像這樣:

StateOwner stateOwner = new StateOwner();

stateOwner.addStateListener(new StateChangeListener() {

    public void onStateChange(State oldState, State newState) {

        // do something with the old and new state.

    }

});

首先,建立一個StateOwner例項,然後把StateChangeListener介面的匿名實現類被作為一個監聽器新增到StateOwner例項中。

但是在java8中,你可以使用javaLambda表示式來新增一個事件監聽器,就像這樣:

StateOwner stateOwner = new StateOwner();   

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


lambda表示式就是下面的這一部分:

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

我們要求lambda表示式要匹配addStateListener()方法的引數型別,如果lambda表示式匹配該引數型別(在這裡,即:StateChangeListener介面)。那麼,這個lambda表示式就會被轉化為一個實現了StateChangeListener介面的函式。

java的lambda表示式只適用於那些匹配單一方法介面的類。在上面的例子中,lambda表示式被當做引數使用,而引數型別是StateChangeListener介面。這個介面只有一個方法,因此該lambda表示式能夠成功地匹配StateChangeListener介面。



2.使lambda表示式和介面匹配matching  lambdas to interface

一個單方法的介面有時也被稱為 函式式介面,把一個java表示式和一個函式式介面相匹配要分成下面三步:

   .這個介面只有一個抽象方法嗎???

  .lambda表示式的引數和方法的引數匹配嗎???

  .lambda表示式的返回值型別和方法的返回值型別匹配嗎???

如果這三個問題的答案都是Yes的話,那麼這個給定的lambda表示式就能成功地匹配此介面。


3.含有預設和靜態方法的介面

從java8開始,一個java介面裡面可以同時包含預設方法和靜態方法。預設方法和靜態方法的實現可以直接在介面的宣告。這也就意味著,java的lambda表示式也可以適用於包含超過一個方法的介面-只要該介面裡只有一個未實現的方法即可。

換句話說,介面仍然是函式式介面,即使他包含預設函式和靜態函式,只要這個介面理由只有一個未實現的方法就行。

下面的這個介面,可以用一個lambda表示式實現:

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);

    }

}


4.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表示式又被說成是無狀態的。


5.Lambda表示式的型別推斷

在java8之前,如果你要實現一個介面實現,你不得不指定要實現哪個介面。這裡有一個匿名介面實現的例子:

stateOwner.addStateListener(new StateChangeListener() {

    public void onStateChange(State oldState, State newState) {

        // do something with the old and new state.

    }

});

但是如果使用lambda表示式的話,它的型別可以根據附近的程式碼推到出來。例如,引數的介面型別可以從方法的宣告addStateListener()那裡推匯出來.這被稱為型別推斷。

編譯器會查詢這個引數所屬的型別-在這個例子中,就是方法定義。下面這個例子就可以看出,我們在lambda表示式中並未提及StateChangeListener介面:

stateOwner.addStateListener(

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

);

在lambda表示式裡面,引數型別也能直接推斷出來。在上面這個例子中,編譯器就是根據onStateChange()方法的宣告來確定引數型別的。因此,lambda表示式中的oldState和newState的型別也可以從onStateChange()方法的宣告中推斷出來。


6.Lambda引數

由於java的lambda表示式實際上就是一個方法,所以,lambda表示式可以像方法一樣去接收引數。 上面例子中的lambda表示式中的(oldState,newState)就表明了lambda表示式要接收的引數。這些引數都得和介面中方法的引數保持一致。在這個例子中,這些引數必須要匹配StateChangeListener介面的onStateChange()方法中的引數。

public void onStateChange(State oldState, State newState);


7.零引數

如果和lambda表示式匹配的方法不接收任何引數,那你可以寫個像這樣的lambda表示式:

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

這就是表明該lambda表示式不接收任何引數。


8. 一個引數

如果匹配lambda表示式的方法只接收一個引數的話,你可以寫一個這樣的lambda表示式:

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

可以注意到,引數被放在圓括號裡面。另外,當一個lambda表示式接收一個引數時,你可以省略圓括號,就像這樣:

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


9.多個引數

如果lambda表示式匹配的方法接收多個引數的話,那麼引數需要放置到圓括號裡,就像下面的這樣:

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

只有當方法只接收一個引數時,圓括號才能被省略。


10.引數型別

有時為lambda表示式指定引數型別是非常必要的(如果這個編譯器不能從介面方法中推斷出引數型別的話)。不要擔心,如果出現這種情況,編譯器會提示你。這裡有個lambda表示式引數型別的例子:

(Car car) -> System.out.println("The car is: " + car.getName());

正如你看到的,名為car的引數型別被寫在了引數名字的前面,就像你在方法裡面宣告瞭一個引數一樣。


11.Lambda函式體

lambda表示式體,也即是:它所表示的方法體被指定在了"->"的右邊,這裡就是一個例子:

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

如果你的lambda表示式需要由多行構成的話,你可以把lambda表示式體放在{}中括號裡面包起來,這裡有個例子:

(oldState, newState) -> {

    System.out.println("Old state: " + oldState);

    System.out.println("New state: " + newState);

  }


12.從Lambda表示式返回一個值

你可以從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表示式的返回值。

13.變數捕捉

在某些情況下,lambda表示式也能夠訪問那些宣告在lambda函式體外面的變數。我這裡有一段程式碼:

  Java 可以獲取以下三種型別的變數:

       .區域性變數

      .例項變數

      .靜態變數

  我們將在下個章節中詳細講述。

13.1本地區域性變數捕獲

java的lambda表示式可以獲取在lambda函式體外宣告的本地區域性變數的值。為了展示它,我們先看一個單一方法的介面:

public interface MyFactory {

            public String create(char[] chars);

    }

現在,我們看一下,lambda表示式如何實現這個介面的:

MyFactory myFactory = (chars) -> {

    return new String(chars); 

  };

現在這個lambda表示式僅僅是引用了傳遞給它的引數值,但是我們也能換一種寫法,下面有一個更新版本:

String myString = "Test";

  MyFactory myFactory = (chars) -> {

    return myString + ":" + new String(chars);

  };

正如你所看到的,在lambda表示式體重引用了本地區域性變數myString的值。

3.2 例項變數捕獲

lambda表示式也能使用lambda表示式所在物件的例項變數。下面有個例子:

public class EventConsumerImpl {

    private String name = "MyConsumer";

    public void attach(MyEventProducer eventProducer){

        eventProducer.listen(e -> {

            System.out.println(this.name);

        });

    }

}

可以注意到,對name的引用this.name位於lambda表示式的內部。這樣做的話,lambda表示式就能夠在EventConsumerImpl的內部訪問name例項量(域變數)。更甚之,在獲取到它的訪問之後,我們也能夠改變此例項變數的值,並且它的值也能被對映到lambda表示式的內部。

這裡的this,實際上是一種域,它和java的匿名介面實現有所不同。一個匿名介面實現也可以有它自己例項變數,並且可以通過this應用進行訪問。但是,對於lambda表示式來說,lambda表示式不能有自己的例項變數,因此,this總是指向當前物件。

注意: 以上的事件消費者的設計不是特別的優美簡潔,我那樣做僅僅是為了描述一個訪問例項變數的場景。

13.3 靜態變數捕捉

一個lambda表示式也能捕捉靜態變數。這並不吃驚,因為靜態變數在整個應用的內部都能訪問靜態變數(只要該靜態變數是可訪問的 )。

下面有一個例子:

public class EventConsumerImpl {

    private static String someStaticVar = "Some text";

    public void attach(MyEventProducer eventProducer){

eventProducer.listen(e -> {

    System.out.println(someStaticVar);

});

    }

}

靜態變數的值在被lambda表示式捕捉過之後,也允許修改。


14.方法引用作為lambda表示式

在上面的例子中,你的lambda表示式所做的事情就是 呼叫另一個方法並把引數傳遞給該lambda表示式。但是java的lambda表示式實現,提供了一種更簡單的方式去表示一個方法呼叫。

首先,這裡有一個單一方法的介面:

public interface MyPrinter{

    public void print(String s);

}

緊接著,我們建立一個實現了MyPrinter介面的lambda表示式例項:

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

由於該lambda表示式只有一個單一的語句構成,所以實際上我們可以不用{}中括號來包裹。並且,由於這裡lambda表示式只有一個引數,所以我們

可以忽略掉引數的()標籤。就像這樣:

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

由於上面的lambda表示式做的事情就是把string引數轉寄給System.out.println()方法,所以我們可以用一個方法引用來替換上面的lambda表示式。

just looks:

MyPrinter myPrinter = System.out::println;

可以看到,倆個冒號:: ,這就是向編譯器表明,這是一個方法引用並且這個方法引用就是倆個冒號::之後的東西。不管擁有該引用方法的是類還是物件,它都是位於倆個冒號::之前的類或者物件。

你可以參考下面的幾種方法型別:

  .靜態方法

  .例項方法

  .引數物件上的例項方法

  .構造器

  上面的每一種方法引用都會在下面的章節中講述。


14.1 靜態方法引用

最簡單的方法引用就是靜態方法引用。這是一個簡單的單函式介面:

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);

    }

}

下面我們就用一個lambda表示式來呼叫這個方法:

Finder finder = MyClass::doFind;

由於Finder.find()方法的引數和MyClass.doFind()方法匹配,所以就能建立一個實現Finder.find()方法的lambda表示式,同時 引用了MyClass.doFind()方法。


14.2 引數方法引用



14.3例項方法引用

第三點,我們也能從一個lambda表示式的定義處引用一個例項方法,首先,先看一個單方法介面定義:

public interface Deserializer {

    public int deserialize(String v1);

}

這個介面代表了一個能把一個String序列化為一個int的元件。

現在,看看這個StringConverter類:

public class StringConverter {

    public int convertToInt(String v1){

        return Integer.valueOf(v1);

    }

}


convertToInt()方法和Deserializer類的deserialize()方法有著同樣的方法簽名,正是由於這樣,我們可以建立一個StringConverter

例項,,並且從一個lambda表示式中引用它:

StringConverter stringConverter = new StringConverter();

Deserializer des = stringConverter::convertToInt;

第一行的程式碼建立了一個StringConverter例項物件,第二行的lambda表示式引用了StringConverter 例項物件的convertToInt()方法。


14.4構造方法引用

最後,我們也能引用一個類的構造方法,你只需要在::new 的前面寫上類名即可。就像這樣:

MyClass::new

如果想看一下,作為lambda表示式如何使用構造方法的話,看下這個介面的定義:

public interface Factory {

    public String create(char[] val);

}

這個create()方法能夠匹配String類的某一個構造方法的方法簽名,因此,這個構造方法可以作為一個lambda表示式使用。這裡有一個例子:

Factory factory = String::new;

上面的這段程式碼其實就等同於:

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

相關文章