stream and lambda(1) - lambda 簡介

語言: CN / TW / HK

java8 開始, lambda 正式在 JAVA 中出現。有人說它類似於其他函數語言程式設計語言中的閉包,有人說它是一種語法糖。讓我們來一步一步瞭解它。

lambda 表示式的結構

lambda 表示式的結構大致如下,同時遵循偷懶原則:能省則省,能不寫就不寫。

(型別)(引數1, 引數2, …) -> {方法實現}

  • 型別一般情況下可以自動推匯出來,此時可以省略不寫。如果存在多個型別滿足條件,則需指定型別。
  • 可以有零個或多個引數,引數寫在圓括號內。
  • 沒有引數時只需寫 () ,只有一個引數且型別可以推導時可以不寫圓括號。
  • 引數的型別可以省略不寫,根據上下文來推斷。例如: (int a)->{}(a)->{}a->{} 效果相同。
  • 方法實現可以有零條或者多條語句,方法實現寫在花括號內。
  • 如果方法實現只有一條語句,則花括號可以省略。如: a -> return a>1
  • 如果方法實現只有一條語句, return 關鍵字可以省略。如: a -> a==1
  • 如果方法實現有多條語句,則需寫在花括號內。
  • 如果方法實現只有一條語句,且那一條的內容是某個方法,且方法引數是入參,可簡寫。如: (int a) -> {return String.valueOf(a)} 等效於 a -> String.valueOf(a) ,也等效於 String::valueOf

從策略模式開始演化

先給定場景:A市有一批人提交了落戶申請,但是A市不像某經濟特區,來了還不是A市人,要滿足一定的條件才可以落戶。

//定義居民物件,包含姓名、年齡、社保月數、認證證書級別
public class Citizen {
    private String name;
    private int age;
    private int socialInsuranceMonth;
    private int certificateLevel;
}

提供初始化資料方法

List<Citizen> initCitizens() {
    List<Citizen> citizens = new ArrayList<>();
    citizens.add(new Citizen("張三", 23, 11, 0));
    citizens.add(new Citizen("李四", 24, 13, 0));
    citizens.add(new Citizen("王五", 25, 15, 2));
    citizens.add(new Citizen("趙六", 26, 8, 3));
    citizens.add(new Citizen("劉七", 35, 12, 3));
    citizens.add(new Citizen("宋八", 41, 0, 2));
    return citizens;
}

定義資料處理框架方法及處理介面

List<Citizen> filterApply(List<Citizen> citizenList, SettleStrategy strategy) {
    List<Citizen> resList = new ArrayList<>();
    for (Citizen citizen : citizenList) {
        if (strategy.test(citizen)) {
            resList.add(citizen);
        }
    }
    return resList;
}

public interface SettleStrategy {
    boolean test(Citizen citizen);
}

策略模式

假設落戶有兩種方式:

  1. 不超過40歲且有國家認證的高於2級的證書
  2. 不超過40歲且累計交滿12個月社保

定義策略實現類

public class CertificateSettleStrategy implements SettleStrategy {
    @Override
    public boolean test(Citizen citizen) {
        return citizen.getAge() <=40 && citizen.getCertificateLevel() > 2;
    }
}

public class SocialInsuranceSettleStrategy implements SettleStrategy {
    @Override
    public boolean test(Citizen citizen) {
        return citizen.getAge() <=40 && citizen.getSocialInsuranceMonth() >= 12;
    }
}

則使用策略模式的寫法大致如下:

public void testStrategy() {
    //證書達標的
    List<Citizen> citizens = filterApply(initCitizens(), new CertificateSettleStrategy());
    System.out.println(citizens);

    //社保達標的
    citizens = filterApply(initCitizens(), new SocialInsuranceSettleStrategy());
    System.out.println(citizens);
}

點評:

從上述程式碼可以看出,使用策略模式時,要求必須有指定的策略類。

雖然好處是有新策略可以直接新增一個策略處理類,無需反覆修改處理邏輯。

但是仍然面臨著策略變化,必須增加新策略類的問題。

匿名內部類

使用匿名內部類,可無需再定義策略處理類。可能你在某些業務地方用過,比如往執行緒池裡丟一個 Runnable ,你可能不會再單獨定義一個子類。

public void testAnonymous() {
    List<Citizen> citizens = filterApply(initCitizens(), new SettleStrategy() {
        @Override
        public boolean test(Citizen citizen) {
            return citizen.getAge() <=40 && citizen.getSocialInsuranceMonth() >= 12;
        }
    });

    System.out.println(citizens);
}

點評:

從上述程式碼可以看出,使用匿名內部類時,可以允許自己不再定義單獨的類,但還需顯式的 new 子類並 override

相比策略模式,程式碼更集中,也不必在專案裡維護太多的類。缺點則是當處理邏輯較複雜時,寫法不優雅,看起來會雜亂。

此時,比如在 intellij idea 裡,應該會提示你程式碼可以 replace with lambda

lambda 表示式

lambda 的寫法會簡潔不少,遵循上面說的偷懶原則,省略型別、省略引數、省略括號。

public void testLambda() {
    List<Citizen> citizens = filterApply(initCitizens(),
            citizen -> citizen.getAge() <=40 && citizen.getSocialInsuranceMonth() >= 12);
    System.out.println(citizens);
}

點評:

相比匿名內部類,lambda 更近一步。此時,不用再出現 SettleStrategy 字樣,不必再出現 test 方法字樣。

既然如此,那麼,此時就剩最後一個問題了: 為什麼還要定義 SettleStrategy 介面呢?

stream

stream 提供了一系列的便捷處理方法,java8 也提供了一系列的通用的函式式介面,讓我們免去定義一些不必要的介面類。當然,stream 與函式式介面不在本篇的內容範圍內。

通過 stream 的寫法,可以解決 lambda 遺留的那個問題。

public void testStream() {
    Citizen first = initCitizens().stream()
            .filter(citizen -> citizen.getAge() < 35)
            .findFirst()
            .orElse(null);
    System.out.println(first);
}

看,鏈式呼叫,是不是很簡潔易讀?

lambda 表示式與匿名內部類的區別

從上面的介紹可以看出,其實 lambda 表示式和匿名內部類很相似,相似到只是寫法稍作改變。如果從上面所說的偷懶原則來看,lambda 表示式就是匿名實現了函式式介面,並把匿名內部類一步步的偷懶簡化。但是二者從本質上來講,又是不同的。

匿名內部類,本質還是一個類,例項化的是一個物件;lambda 表示式從根本上來說,是一種編譯器會特定處理的語法。

從使用上來看,匿名內部類還是要 new 出來一個例項化物件的,而 this 關鍵字在匿名內部中使用時,指的是內部類,而非外部類;對於 lambda 表示式,this 關鍵字則代表的是外部類(lambda所在的類)。從編譯輸出來看,當使用匿名內部類時,會編譯出一個單獨的內部類的 class;而 lambda 表示式則不會編譯出單獨的 class 出來。

總結

本文總結了 lambda 表示式的結構,同時從策略模式說起,一步步的簡化,逐步引出了 lambda 表示式,並比較不同寫法之間的差異性,同時稍稍介紹了 stream。

lambda 表示式可以讓我們不用去費力的給一個方法、或者一個類命名,讓我們可以以匿名的方式去使用它。但是如果匿名方法裡的處理邏輯太複雜,則不建議使用 lambda 表示式去寫,lambda 所表達的內容應該是簡單易讀的。

注:本文配套程式碼可在 github 檢視: stream-and-lambda