Java SE基礎鞏固(十五):lambda表達式

1 概述

Java8聽說是Java誕生以來最大的一次演進,說實話,對我我的來講沒有什麼特別大的感覺,由於我學Java也就最近一兩年的事,Java8在2014年3月18日發佈,新增的特性確實很是驚豔,在語言特性層面上新增了lambda,Optional,默認方法,Stream API等,在虛擬機層面上新增了G1收集器(不過在Java9以後才改成默認的垃圾收集器)......java

我我的認爲Java8和語言相關的幾個最重要的特性是以下幾個:git

  • lambda表達式和方法引用(實際上是lambda表達式的一種特例)
  • Stream API
  • 接口的默認方法
  • Optinal
  • CompletableFuture

本系列文章的後面幾篇文章會圍繞這幾個主題來展開,今天就先上個開胃菜,lambda表達式!github

2 什麼是lambda表達式

lambda表達式也叫作匿名函數,其基於著名的λ演算得名,關於λ演算,推薦你們去找找關於「丘奇數」相關的資料。Java一直被人詬病的一點就是「囉嗦」,一般爲了實現一個小功能,就不得不編寫大量的代碼,而用其餘的語言例如Python等,也許寥寥幾行代碼就解決了,但支持lambda表達式以後,這一狀況獲得了大大的改善,如今只要使用得當,能夠大大縮減代碼裏,使代碼的目的更加清晰,易讀,純粹。shell

在Java中,不少時候在使用一些API的時候,必需要給出一些接口的實現,但由於該實現其實也就用一次,專門去建立一個新的實現類並不划算,因此通常大多數人採起的措施應該是建立一個匿名實現類,比較典型就是Collections.sort(List list, Comparator<? super T> c)方法,該方法接受一個Comparator類型的參數,Comparator是一個接口,表示「比較器」,若是要使用該方法對集合元素進行排序,就必須提供一個Comparator接口的實現,不然沒法經過編譯。以下所示:編程

Collections.sort(numbers, new Comparator<Integer>() {
    @Override
    public int compare(Integer o1, Integer o2) {
        return o1.compareTo(o2);
    }
});
複製代碼

其實這個實現類的核心只有一行,即return o1.compareTo(o2);但咱們卻不得不編寫其餘「囉嗦」的代碼,若是使用lambda表達式,會是怎麼個樣子呢?設計模式

Collections.sort(numbers, (n1, n2) -> n1.compareTo(n2));
複製代碼

沒錯,就是那麼簡單粗暴,就是一行核心代碼。其餘的好比方法簽名啥的通通能夠省略了,不只簡潔,並且語義也更加清晰,讀起來就好像是說:「sort方法,幫我吧numbers這個序列排個序,排序規則就按照n1.compareTo(n2)的返回值來決定」。如今,是否是感受,寫代碼就像在和計算機對話同樣簡單?但(n1, n2) -> n1.compareTo(n2)這玩意是個什麼鬼?還帶個箭頭?不用着急,下面立刻介紹lambda表達式的語法。app

2.1 lambda表達式的語法

FsV8BQ.png

  • 第一部分是lambda的參數列表,由於Comparator.compare()方法接受兩個參數,因此這裏給出兩個參數n1和n2,能夠省略具體的類型,Java編譯器會自動推斷。
  • 第二部分是箭頭,沒什麼特殊的地方,只是Java語言以爲使用這個,各個語言的實現也不太同樣,例如Python是:號,簡單理解的就當是把參數列表和函數主體分開的東西吧。
  • 第三部分就是函數主體,也就是真正執行邏輯的地方。

若是函數主體僅僅包含一行代碼,能夠省略花括號{}和return關鍵字(若是有的話)。對於咱們的例子,能夠改寫成這樣:ide

Collections.sort(numbers, (n1, n2) -> {return n1.compareTo(n2);});
複製代碼

注意分號!由於此時return n1.compareTo(n2);就是一條普通的Java語句了,必須遵照Java的語法規則。好了,儘管咱們如今明白了lambda語句的語法規則,但還有一個關鍵的問題,就是爲何要這樣寫,換句話說,爲何要有倆參數,這return又是幾個意思?還有到底哪裏纔可使用lambda表達式?說到這,就不得不說一下和lambda息息相關的東西了:函數式接口函數式編程

3 函數式接口

函數式接口是這樣的:只有一個抽象方法的接口就是函數式接口。爲何要特別強調抽象方法呢?Java接口裏聲明的方法不都是抽象方法嗎?在Java8以前,這麼說確實沒有任何問題,但Java8新增了接口的默認方法,能夠在接口裏給出方法的具體實現,這裏先很少說,後面的文章會詳細討論這個東西。函數

lambda表達式僅能夠用在函數式接口上,咱們在上面遇到的Comparator就是一個函數式接口,他只有一個抽象方法:compare(),其方法簽名是這樣的:

int compare(T o1, T o2);
複製代碼

如今來看看 (n1, n2) -> n1.compareTo(n2)這個表達式,是否是發現了什麼?沒錯,其實lambda表達式的參數列表就是對應的函數式接口的抽象方法的參數列表,而且類型能夠省略(編譯器自動推斷),而後n1.compareTo(n2)的返回值是int類型,也符合compare()的方法描述。這樣就算是把lambda表達式和接口的抽象方法簽名匹配成功了,不會出現編譯錯誤。

除此以外,Runnable也是一個函數式接口,它只有一個抽象方法,即run(),run()方法的方法簽名以下所示:

public abstract void run();
複製代碼

不接受任何參數,也沒有返回值。那若是要編寫對應的lambda表達式,該如何作呢?其實很是簡單,下面是一個示例:

Runnable r = () -> {
    System.out.println(Thread.currentThread().getName());
    //do something
};
複製代碼

若是觀察仔細的話,會發現,示例代碼中把這個lambda表達式賦值給了Runnable類型的變量r!通過上面的討論,咱們知道,其實lambda就是一個方法實現(其實叫作函數會更加合適),這條賦值語句看起來就好像是再說:「把方法(函數)賦值給變量!」。若是沒有接觸過函數式編程,會以爲這樣很奇怪,怎麼能把方法賦值給變量呢?計算機就是這樣有意思,老是有各類各樣奇奇怪怪的東西衝擊咱們的思惟!那這有什麼用呢?咱先不說什麼高階函數,科裏化啥的(這些是函數式編程裏的概念),就說一點:意味着咱們能夠把方法(函數)當作變量來使用!即如今方法就是Java世界裏的「一等公民」了!既能夠將其做爲參數傳遞給其餘方法(函數),還能夠將其做爲其餘方法(函數)的返回值(之後會講到具體的案例)

4 策略模式

策略模式是著名的23種設計模式中的一種,關於它的描述,我這裏就很少說了。直接來看個例子吧。

例子是這樣的,如今有一個表明汽車的Car類以及一個Car列表,如今咱們想要篩選列表中符合要求的汽車,爲了應對多變的篩選方法,咱們打算用策略模式來實現功能。

下面是Car類的代碼:

public class Car {

    //品牌
    private String brand;

    //顏色
    private Color color;

    //車齡
    private Integer age;
	
    //三個參數的構造函數以及setter和getter
    
    //顏色的枚舉
    public enum Color {
        RED,WHITE,PINK,BLACK,BLUE;
    }
}

//包含Car對象的列表
List<Car> cars = Arrays.asList(
        new Car("BWM",Car.Color.BLACK, 2),
        new Car("Tesla", Car.Color.WHITE, 1),
        new Car("BENZ", Car.Color.RED, 3),
        new Car("Maserati", Car.Color.BLACK,1),
        new Car("Audi", Car.Color.PINK, 5));
複製代碼

咱們但願用一個方法來封裝篩選的邏輯,其方法簽名僞代碼以下所示:

cars carFilter(cars, filterStrategy);
複製代碼

接下來實現策略模式,下面是相關的代碼:

public interface CarFilterStrategy {
    boolean filter(Car car);
}

public class BWMCarFilterStrategy implements CarFilterStrategy {
    @Override
    public boolean filter(Car car) {
        return "BWM".equals(car.getBrand());
    }
}

public class RedColorCarFilterStrategy implements CarFilterStrategy {

    @Override
    public boolean filter(Car car) {
        return Car.Color.RED.equals(car.getColor());
    }
}
複製代碼

爲了簡單,僅僅實現了兩種篩選策略,第一種是刪選出品牌是「BWM」的汽車,第二種是刪選出顏色爲紅色的汽車。最後來實現carFilter方法,以下所示:

private static List<Car> carFilter(List<Car> cars, CarFilterStrategy strategy) {
    List<Car> filteredCars = new ArrayList<>();
    for (Car car : cars) {
        if (strategy.filter(car)) {
            filteredCars.add(car);
        }
    }
    return filteredCars;
}
複製代碼

最後的最後是測試代碼:

public static void main(String[] args) {
    System.out.println(carFilter(cars, new BWMCarFilterStrategy()));
    System.out.println("----------------------------------------");
    System.out.println(carFilter(cars, new RedColorCarFilterStrategy()));
}
複製代碼

分別實例化兩個策略,將其做爲參數傳遞給carFilter()方法,最終的輸出以下所示:

[Car{brand='BWM', color=BLACK, age=2}]
----------------------------------------
[Car{brand='BENZ', color=RED, age=3}]
複製代碼

確實符合預期。是否是就到此爲止了呢?固然不!咱們發現,其實BWMCarFilterStrategy以及RedColorCarFilterStrategy的實現代碼都很是簡單,僅僅寥寥幾行代碼,並且CarFilterStrategy接口僅僅有一個filter抽象方法,顯然是一個函數式接口,那咱們能不能用lambda表達式來簡化呢?答案是:徹底能夠!並且更加推薦用lambda表達式來簡化這種狀況。

4.1 用lambda表達式來簡化代碼

只要略微作一些修改就好了:

System.out.println(carFilter(cars, car -> "BWM".equals(car.getBrand())));
System.out.println("----------------------------------------");
System.out.println(carFilter(cars, car -> Car.Color.RED.equals(car.getColor())));
複製代碼

這裏再也不使用BWMCarFilterStrategy以及RedColorCarFilterStrategy兩個類了,直接用lambda表達式就好了!最後把這倆實現刪除掉!是否是頓時感受整個項目的代碼清爽了許多?

4.2 須要注意的

其實本小節的例子有些過於特殊了,若是你項目中的策略模式的實現很是複雜,其策略不是簡簡單單的幾行代碼就能解決的,此時要麼進一步封裝代碼,要麼就最好不要用lambda表達式了,由於若是邏輯複雜的話,強行使用lambda不只僅不能簡化代碼,反而會使得代碼更加晦澀。

5 方法引用

最後簡單講一下方法引用吧,方法引用實際上是lambda表達式的一種特殊狀況的表示,語法規則是:

<class name or instance name>:<method name>
複製代碼

若是lambda表達式的主體邏輯僅僅是一個調用方法的語句的話,那麼就能夠將其轉換爲方法引用,以下所示:

//普通的lambda表達式
numbers.forEach(n -> System.out.println(n));
//轉換成方法引用
numbers.forEach(System.out::println);
複製代碼

他倆效果是徹底同樣的,但顯然方法引用更加簡潔,語義也更加明確了,這一語法糖「真香!」。具體的我就很少說了,建議看看《Java8 實戰》一書,裏面有很是很是詳細的介紹。

6 小結

本文簡單介紹了lambda表達式的語法以及使用。lambda表達式確實能大大簡化本來複雜囉嗦的Java代碼,並且更加靈活,語義也更加清晰明瞭,寫代碼的時候就好像用天然語言和計算機對話同樣!但也不是哪裏都能使用的,一個最基本的要求就是:其放置的位置要對應着一個函數式接口。函數式接口即只有一個抽象方法的接口,例如Comparator,Runnable等。除此以外,使用lambda表達式的時候,其主體邏輯最好不要超過10行,不然最好仍是換一種方式來實現,這裏10行並非那麼嚴格,具體狀況還要具體分析。方法引用是一種特殊狀況下的lambda表達式的表示方法,能夠理解爲是lambda的一個語法糖,其語義更加明確,語法也更加簡潔,用起來仍是很是舒服的!

最後,做爲一個補充,來簡單看看JDK內置的一些通用性比較強的函數式接口,這些接口都在java.util.function包下,我沒數過,咋一看估計得有40多個吧。經常使用的有Function,Predicate,Consumer,Supplier等。Function的抽象方法的方法簽名以下所示:

R apply(T t); //T,R是泛型
複製代碼

簡單從語義上來看,就是傳入一個T類型的值,而後apply函數將其轉換成R類型的值,即一對一映射。其餘的接口就不作介紹了。

7 參考資料

《Java8 實戰》

阿隆佐.丘奇的天才之做——lambda演算中的數字

相關文章
相關標籤/搜索