1. 程式人生 > 程式設計 >Java SE基礎鞏固(十五):lambda表示式

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

1 概述

Java8據說是Java誕生以來最大的一次演進,說實話,對我個人來說沒有什麼特別大的感受,因為我學Java也就最近一兩年的事,Java8在2014年3月18日釋出,新增的特性確實非常驚豔,在語言特性層面上新增了lambda,Optional,預設方法,Stream API等,在虛擬機器器層面上新增了G1收集器(不過在Java9之後才改為預設的垃圾收集器)......

我個人認為Java8和語言相關的幾個最重要的特性是如下幾個:

  • lambda表示式和方法引用(其實是lambda表示式的一種特例)
  • Stream API
  • 介面的預設方法
  • Optinal
  • CompletableFuture

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

2 什麼是lambda表示式

lambda表示式也叫做匿名函式,其基於著名的λ演算得名,關於λ演算,推薦大家去找找關於“丘奇數”相關的資料。Java一直被人詬病的一點就是“囉嗦”,通常為了實現一個小功能,就不得不編寫大量的程式碼,而用其他的語言例如Python等,也許寥寥幾行程式碼就解決了,但支援lambda表示式之後,這一情況得到了大大的改善,現在只要使用得當,可以大大縮減程式碼裡,使程式碼的目的更加清晰,易讀,純粹。

在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表示式的語法。

2.1 lambda表示式的語法

FsV8BQ.png

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

如果函式主體僅僅包含一行程式碼,可以省略花括號{}和return關鍵字(如果有的話)。對於我們的例子,可以改寫成這樣:

Collections.sort(numbers,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",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演算中的數字