Java函數式編程快速入門:Lambda表達式與Stream API

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

Java函數式編程快速入門:Lambda表達式與Stream API

函數式編程思想

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

<code>(x + y) * z/<code>

一般的編程思路是:

<code>addResult = x + yresult = addResult * 3/<code>

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

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

<code>result = multiply(add(x, y), z)/<code>

我們再做進一步的轉換:

<code>result = add(x, y).multiply(z)/<code>

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

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

  1. 實現單個函數,將零到多個輸入轉換成零到多個輸出。比如add這種帶有映射關係的函數,它將兩個輸入轉化為一個輸出。
  2. 將多個函數連接起來,實現所需業務邏輯。比如,將add、multiply連接到一起。

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

Lambda表達式的構造

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

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

<code>(parameters) -> {  body }/<code> 

Lambda表達式主要包括一個箭頭->符號,兩邊連接著輸入參數和函數體。我們再看看幾個Lambda表達式的例子:

<code>// 1. 無參數,返回值為5  () -> 5    // 2. 接收一個參數(int類型),將其乘以2,返回一個intx -> 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;}/<code>

可以看到,這幾個例子都有一個->,表示這是一個函數式的映射,相對比較靈活的是左側的輸入參數和右側的函數體。下圖為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、泛型等知識,可以參考我前兩篇文章: 和 。

<code>@FunctionalInterfaceinterface AddInterface 
{ 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); }}/<double>/<integer>/<code>

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

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

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

在業務邏輯中這樣調用:doubleResult = new MyAdd().add(1.1d, 2.2d);。或者是使用匿名類,連MyAdd這個名字省去,直接實現AddInterface並調用:

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

聲明類並實現接口和使用匿名類這兩種方法是Lambda表達式出現之前Java開發者經常使用的兩種方法。實際上我們想實現的邏輯僅僅是一個a + b,其他行代碼其實都是冗餘的,都是為了給編譯器看的,並不是為了給程序員看的。有了比較我們會發現,Lambda表達式簡潔優雅的優勢就凸顯出來了。

為了方便大家使用,Java內置了一些的函數式接口,放在java.util.function包中,比如Predicate、Function、BinaryOperator等,開發者可以根據自己需求去實現這些接口。這裡簡單展示一下兩個接口。

Predicate對輸入進行判斷,符合給定邏輯則返回true,否則返回false。

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

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

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

一些底層的框架性代碼提供了一些函數式接口供開發者調用,很多框架提供給開發者的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表達式來完成對應的邏輯。/<integer>

<code>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));/<integer>/<string>/<code>

這段代碼中,數據先經過stream方法被轉換為一個Stream類型,後經過filter、map、collect等處理邏輯,生成我們所需的輸出。各個操作之間使用英文點號.來連接,這種方式被稱作方法鏈(Method Chaining)或者鏈式調用。數據的鏈式調用可以被抽象成一個管道(Pipeline),如下圖所示。

Java函數式編程快速入門:Lambda表達式與Stream API

我們深挖一下Java Stream的源碼,發現filter的參數正是前文所說的Predicate函數式接口,map的參數是前文提到的Function函數式接口。當處理具體的業務時,就是使用Lambda表達式來實現這些函數式接口。

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

上面兩行是Java Stream的源碼,其中?是泛型通配符,主要是為了對泛型做一些安全性上的限制,有興趣的讀者可以自行去了解泛型的的通配符。

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

小結

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


分享到:


相關文章: