天天看點

java lambda表達式_Java函數式程式設計快速入門: Lambda表達式與Stream API

函數式程式設計(Functional Programming)是一種程式設計範式。它已經有近60年的曆史,因其更适合做并行計算,近年來開始受到大資料開發者的廣泛關注。Python、JavaScript等當紅語言對函數式程式設計支援都不錯,Scala更是以函數式程式設計的優勢在大資料領域攻城略地,即使是老牌的Java為了适應函數式程式設計,也加大對函數式程式設計的支援。未來的程式員或多或少都要了解一些函數式程式設計思想。本文抛開一些數學推理等各類複雜的概念,從使用的角度帶領讀者入門函數式程式設計。

函數式程式設計思想

在介紹函數式程式設計前,我們可以先回顧傳統的程式設計範式如何解決一個數學問題。假設我們想求解一個數學表達式:

(x + y) * z
           

一般的程式設計思路是:

addResult = x + y
result = addResult * 3
           

在這個例子中,我們要先求解中間結果,存儲到中間變量,再進一步求得最終結果。這僅僅是一個簡單的例子,在更多的程式設計實踐中,程式員必須告訴計算機每一步去執行什麼指令,需要聲明哪些中間變量等。因為計算機無法了解複雜的概念,隻能聽從程式員的指揮。

中學時代,我們的數學課上曾花費大量時間講解函數,函數y = f(x)指對于自變量x的映射。函數式程式設計的思想正是基于數學中對函數的定義。其基本思想是,在使用計算機求解問題時,我們可以把整個計算過程定義為不同的函數。比如,将這個問題轉化為:

result = multiply(add(x, y), z)
           

我們再做進一步的轉換:

result = add(x, y).multiply(z)
           

傳統思路要建立中間變量,要分步執行,而函數式程式設計的形式與數學表達式形式更為相似。人們普遍認為,這種函數式的描述更接近人類自然語言。

如果要實作這樣一個函數式程式,主要需要兩步:

  1. 實作單個函數,将零到多個輸入轉換成零到多個輸出。比如

    add

    這種帶有映射關系的函數,它将兩個輸入轉化為一個輸出。
  2. 将多個函數連接配接起來,實作所需業務邏輯。比如,将

    add

    multiply

    連接配接到一起。

接下來我們通過Java語言來展示如何實踐函數式程式設計思想。

Lambda表達式的構造

數理邏輯領域有一個名為λ演算的形式系統,主要研究如何使用函數來表達計算。一些程式設計語言将這個概念應用到自己的平台上,期望能實作函數式程式設計,取名為Lambda表達式(λ的英文拼寫為Lambda)。

我們先看一下Java的Lambda表達式的文法規則:

(parameters) -> {
  body 
}
           

Lambda表達式主要包括一個箭頭

->

符号,兩邊連接配接着輸入參數和函數體。我們再看看幾個Lambda表達式的例子:

// 1. 無參數,傳回值為5  
() -> 5  

// 2. 接收一個參數(int類型),将其乘以2,傳回一個int
x -> 2 * x

// 3. 接受2個參數(int類型),傳回他們的差
(x, y) -> x – y  

// 4. 接收2個int型整數,傳回他們的和
(int x, int y) -> x + y

// 5. 接受一個String對象,在控制台列印,不傳回任何值
(String s) -> { System.out.print(s); }

// 6. 參數為圓半徑,傳回圓面積,傳回值為double類型
(double r) -> {
    double pi = 3.1415;
    return r * r * pi;
}
           

可以看到,這幾個例子都有一個

->

,表示這是一個函數式的映射,相對比較靈活的是左側的輸入參數和右側的函數體。下圖為Java Lambda表達式的一個拆解示意圖,這很符合數學中對一個函數做映射的思維方式。

java lambda表達式_Java函數式程式設計快速入門: Lambda表達式與Stream API

接下來我們來了解一下輸入參數和函數體的一些使用規範。

輸入參數

  • Lambda表達式可以接收零到多個輸入參數。
  • 程式員可以提供輸入類型,也可以不提供類型,讓程式設計語言根據上下文幫忙去推斷。
  • 參數可以放在圓括号

    ()

    中,多個參數通過英文逗号

    ,

    隔開。如果隻有一個參數,且類型可以被推斷,可以不使用圓括号

    ()

    。空圓括号表示沒有輸入參數。

函數體

  • 函數體可以有一到多行語句,是函數的核心處理邏輯。
  • 當函數體隻有一行内容,且該内容正是需要輸出的内容,可以不使用花括号

    {}

    ,直接輸出。
  • 當函數體有多行内容,必須使用花括号

    {}

  • 輸出的類型與所需要的類型相比對。

至此,我們可以大緻看出,Lambda表達式能夠實作将零到多個輸入轉換為零到多個輸出的映射,即實作了函數式程式設計的第一步,定義單個的函數。

Functional Interface

通過前面的幾個例子,我們大概知道Lambda表達式的内部結構了,那麼Lambda表達式到底是什麼類型呢?在Java中,Lambda表達式是有類型的,它是一個

interface

。确切的說,Lambda表達式實作了一個函數式接口(Functional Interface),或者說,前面提到的一些Lambda表達式都是函數式接口的具體實作。

函數式接口是一種

interface

,并且它隻有一個虛函數。因為這種

interface

隻有一個虛函數,是以英文中被稱為Single Abstract Method(SAM)類型接口,這也意味着這個接口對外隻提供這一個函數的功能。如果我們想自己設計一個函數式接口,我們應該給這個接口添加

@FunctionalInterface

注解。編譯器會根據這個注解確定該接口确實是函數式接口,當我們嘗試往該接口中添加超過一個虛函數方法時,編譯器會報錯。下面的例子中,我們自己設計一個加法的函數式接口

AddInterface

,然後實作這個接口。

關于

interface

<T>

泛型等知識,可以參考我前兩篇文章:繼承和泛型。

@FunctionalInterface
interface AddInterface<T> {
    T add(T a, T b);
}

public class FunctionalInterfaceExample {

    public static void main( String[] args ) {

        AddInterface<Integer> addInt = (Integer a, Integer b) -> a + b;
        AddInterface<Double> addDouble = (Double a, Double b) -> a + b;

        int intResult;
        double doubleResult;

        intResult = addInt.add(1, 2);
        doubleResult = addDouble.add(1.1d, 2.2d);
    }
}
           

有了函數式接口的定義,我們知道在實作一個Lambda表達式時,Lambda表達式實際上是在實作這個函數式接口中的虛函數,Lambda表達式的輸入類型和傳回類型要與虛函數定義的類型相比對。

假如沒有Lambda表達式,我們仍然可以實作這個函數式接口,隻不過代碼比較臃腫。首先,我們需要聲明一個類來實作這個接口,可以是下面這樣的一個類:

public static class MyAdd implements AddInterface<Double> {
    @Override
    public Double add(Double a, Double b) {
            return a + b;
    }
}
           

在業務邏輯中這樣調用:

doubleResult = new MyAdd().add(1.1d, 2.2d);

。或者是使用匿名類,連

MyAdd

這個名字省去,直接實作

AddInterface

并調用:

doubleResult = new AddInterface<Double>(){
    @Override
    public Double add(Double a, Double b) {
        return a + b;
    }
}.add(1d, 2d);
           

聲明類并實作接口和使用匿名類這兩種方法是Lambda表達式出現之前Java開發者經常使用的兩種方法。實際上我們想實作的邏輯僅僅是一個

a + b

,其他行代碼其實都是備援的,都是為了給編譯器看的,并不是為了給程式員看的。有了比較我們會發現,Lambda表達式簡潔優雅的優勢就凸顯出來了。

為了友善大家使用,Java内置了一些的函數式接口,放在

java.util.function

包中,比如

Predicate

Function

BinaryOperator

等,開發者可以根據自己需求去實作這些接口。這裡簡單展示一下兩個接口。

Predicate

對輸入進行判斷,符合給定邏輯則傳回

true

,否則傳回

false

@FunctionalInterface
public interface Predicate<T> {
    // 判斷輸入的真假,傳回boolean
    boolean test(T t);
}
           

Function

接收一個類型T的輸入,傳回一個類型R的輸出。

@FunctionalInterface
public interface Function<T, R> {
     // 接收一個類型T的輸入,傳回一個類型R的輸出
     R apply(T t);
}
           

一些底層的架構性代碼提供了一些函數式接口供開發者調用,很多架構提供給開發者的API其實就是類似上面的函數式接口,開發者通過實作接口來完成自己的業務邏輯。Spark和Flink對外提供的Java API其實就是這種函數式接口。

Java Stream API

流(Stream)是Java 8 的另外一大亮點,它與

java.io

包裡的

InputStream

OutputStream

是完全不同的概念,也不是Flink、Kafka等大資料實時進行中的資料流。它專注于對集合(Collection)對象的操作,是借助Lambda表達式的一種應用。通過Java Stream,我們可以體驗到Lambda表達式帶來的程式設計效率的提升。

我們看一個簡單的例子,這個例子首先過濾出非空字元串,然後求得每個字元串的長度,最終傳回為一個

List<Integer>

。代碼使用了Lambda表達式來完成對應的邏輯。

List<String> strings = Arrays.asList(
  "abc", "", "bc", "12345",
  "efg", "abcd","", "jkl");

List<Integer> lengths = strings
  .stream()
  .filter(string -> !string.isEmpty())
  .map(s -> s.length())
  .collect(Collectors.toList());

lengths.forEach((s) -> System.out.println(s));
           

這段代碼中,資料先經過

stream

方法被轉換為一個

Stream

類型,後經過

filter

map

collect

等處理邏輯,生成我們所需的輸出。各個操作之間使用英文點号

.

來連接配接,這種方式被稱作方法鍊(Method Chaining)或者鍊式調用。資料的鍊式調用可以被抽象成一個管道(Pipeline),如下圖所示。

java lambda表達式_Java函數式程式設計快速入門: Lambda表達式與Stream API

我們深挖一下Java Stream的源碼,發現

filter

的參數正是前文所說的

Predicate

函數式接口,

map

的參數是前文提到的

Function

函數式接口。當處理具體的業務時,就是使用Lambda表達式來實作這些函數式接口。

Stream<T> filter(Predicate<? super T> predicate);
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
           

上面兩行是Java Stream的源碼,其中

?

是泛型通配符,主要是為了對泛型做一些安全性上的限制,有興趣的讀者可以自行去了解泛型的的通配符。

Java Stream是應用Lambda表達式的最佳案例,Stream管道和鍊式調用解決了本文最初提到的函數式程式設計第二個問題:将多個函數連接配接起來,實作所需業務邏輯。

小結

函數式程式設計更符合數學上函數映射的思想。具體到程式設計語言層面,我們可以使用Lambda表達式來快速編寫函數映射,函數之間通過鍊式調用連接配接到一起,完成所需業務邏輯。Java的Lambda表達式是後來才引入的,而Scala天生就是為函數式程式設計所設計。由于函數式程式設計在并行處理方面的優勢,正在被大量應用在大資料計算領域。

繼續閱讀