重識Java8函數式編程

前言

最近真的是太忙忙忙忙忙了,很久沒有更新文章了。最近工作中看到了幾段關於函數式編程的代碼,但是有點費解,於是就準備總結一下函數式編程。很多東西很簡單,但是如果不總結,可能會被它的各種變體所困擾。接觸Lambda表達式已經很久了,但是也一直是處於照葫蘆畫瓢的階段,所以想自己去編寫相關代碼,也有些捉襟見肘。

1. Lambda表達式的不同形式

// 基本形式
參數 -> 主體

1.1 形式一

Runnable noArguments = () -> System.out.println("Hello World");

該形式的Lambda表達式不包含參數,使用空括號()表示沒有參數。它實現了Runnable接口,該接口也只有一個run方法,沒有桉樹,且返回類型為void。

1.2 形式二

ActionListener oneArgument = event -> System.out.println("button clicked");

該形式的Lambda表達式包含且只包含一個參數,可省略參數的符號。

1.3 形式三

Runnable multiStatement = () -> {
	System.out.print("Hello"); 
    System.out.println(" World"); 
};

Lambda表達式的主體不僅可以使一個表達式,而且也可以是一段代碼塊,使用大括號{}將代碼塊括起來。該代碼塊和普通方法遵循的規則別無二致,可以用返回或拋出異常來退出。只有以行代碼的Lambda表達式也可以使用大括號,用以明確Lambda表達式從何處開始,到哪裡結束。

1.4 形式四

BinaryOperator<Long> add = (x, y) -> x + y;

Lambda表達式也可以表示包含多個參數的方法,上面的Lambda表達式並不是將兩個数字相加,而是創建了一個函數,用來計算兩個数字相加的結果。變量add的類型時BinaryOperator ,它不是兩個数字的和,而是將兩個数字相加的那行代碼。

1.5 形式五

BinaryOperator<Long> addExplicit = (Long x, Long y) -> x + y;

到目前為止,所有Lambda表達式中的參數類型都是由編譯器推斷得出的。但有時最好也可以显示聲明參數類型,此時就需要使用小括號將參數括起來,多個參數的情況也是如此。

2. 引用值,而不是變量

如果你曾使用過匿名內部類,也許遇到過這樣的情況:需要引用它所在方法里的變量。這是,需要將變量聲明為final。

final String name = getUserName(); 
button.addActionListener(new ActionListener() {
	public void actionPerformed(ActionEvent event) { 
        System.out.println("hi " + name); 
    } 
});

將變量聲明為 final,意味着不能為其重複賦 值。同時也意味着在使用 final 變量時,實際上是在使用賦給該變量的一個特定的值。

Java 8 雖然放鬆了這一限制,可以引用非 final 變量,但是該變量在既成事實上必須是 final(意思就是你不能再次對該變量賦值)。雖然無需將變量聲明為 final,但在 Lambda 表達式中,也無法用作非終態變量。如 果堅持用作非終態變量,編譯器就會報錯。 既成事實上的 final 是指只能給該變量賦值一次。換句話說,Lambda 表達式引用的是值, 而不是變量。

例如:

String name = getUserName(); 
button.addActionListener(event -> System.out.println("hi " + name));

3. 函數接口

在 Java 里,所有方法參數都有固定的類型。假設將数字 3 作為參數傳給一個方法,則參數 的類型是 int。那麼,Lambda 表達式的類型又是什麼呢?

使用只有一個方法的接口來表示某特定方法並反覆使用,是很早就有的習慣。使用 Swing 編寫過用戶界面的人對這種方式都不陌生,這裏無需再標新立異,Lambda 表達式也使用同樣的技巧,並將這種接口稱為函數接口。

接口中單一方法的命名並不重要,只要方法簽名和 Lambda 表達式的類型匹配即可。可在函數接口中為參數起一個有意義的名字,增加代碼易讀性,便於更透徹 地理解參數的用途。

3.1 Java中重要的函數接口

接口 參數 返回類型 示例
Predicate T boolean 判斷是否
Consumer T void 輸出一個值
Function<T,R> T T 獲得對象的名字
Supplier None T 工廠方法
UnaryOperator T T 邏輯非(!)
BinaryOperator (T, T) T 求兩個數的乘積(*)

3.2 函數接口定義

定義函數接口需要使用到註解@FunctionalInterface

例如:

@FunctionalInterface
public interface MyFuncInterface {
	void print();
}

使用:

public class MyFunctionalInterfaceTest {
    public static void main(String[] args) {
        doPrint(() -> System.out.println("java"));
    }

    public static void doPrint(MyFuncInterface my) {
        System.out.println("請問你喜歡什麼編程語言?");
        my.print();
    }
}

說明:

這隻是一個很簡單的例子,有人覺得為什麼要搞這麼複雜,去定義一個接口?這個問題還是讀者在平時的工作中去感悟吧,總之,先學會怎麼用它。不至於看了別人寫的代碼都看不懂。

至於我個人的理解,可以簡單聊聊。以前寫過JavaScript,裏面有一種語法就是將自定義函數B作為參數傳遞到另外一個函數A裏面,在函數A裏面會執行你自定義的函數B邏輯,我當時就非常喜歡這種特性,因為每個人關於函數B的實現可能不一樣,亦或者場景不一樣也會導致函數B的實現不一樣。我覺得Java8的這個函數式編程就是對這一特性的補充。

4. 流

流的常用操作有很多,例如collect(toList())mapfiltermaxmin等,下面介紹一下flatMapreduce

4.1 flatMap

flatMap 方法可用 Stream 替換值,然後將多個 Stream 連接成一個 Stream。

List<Integer> together = Stream.of(asList(1, 2), asList(3, 4)) 				 
    .flatMap(numbers -> numbers.stream())
    .collect(toList()); 
assertEquals(asList(1, 2, 3, 4), together);

調用 stream 方法,將每個列錶轉換成 Stream 對象,其餘部分由 flatMap 方法處理。 flatMap 方法的相關函數接口和 map 方法的一樣,都是 Function 接口,只是方法的返回值 限定為 Stream 類型罷了。

4.2 reduce

reduce 操作可以實現從一組值中生成一個值。對於 count、min 和 max 方 法,因為常用而被納入標準庫中。事實上,這些方法都是 reduce 操作。

如何通過 reduce 操作對 Stream 中的数字求和。以 0 作起點——一個空Stream 的求和結果,每一步都將 Stream 中的元素累加至 accumulator,遍歷至 Stream 中的 最後一個元素時,accumulator 的值就是所有元素的和。

int count = Stream.of(1, 2, 3)
    .reduce(0, (acc, element) -> acc + element); 
assertEquals(6, count);

Lambda 表達式的返回值是最新的 acc,是上一輪 acc 的值和當前元素相加的結果。reducer 的類型是前面已介紹過的 BinaryOperator。

5. Optional

reduce 方法的一個重點尚未提及:reduce 方法有兩種形式,一種如前面出現的需要有一 個初始值,另一種變式則不需要有初始值。沒有初始值的情況下,reduce 的第一步使用 Stream 中的前兩個元素。有時,reduce 操作不存在有意義的初始值,這樣做就是有意義的,此時,reduce 方法返回一個 Optional 對象。

Optional 是為核心類庫新設計的一個數據類型,用來替換 null 值。人們對原有的 null 值有很多抱怨。人們常常使用 null 值表示值不存在,Optional 對象能更好地表達這個概念。使用 null 代 表值不存在的最大問題在於 NullPointerException。一旦引用一個存儲 null 值的變量,程 序會立即崩潰。使用 Optional 對象有兩個目的:首先,Optional 對象鼓勵程序員適時檢查變量是否為空,以避免代碼缺陷;其次,它將一個類的 API 中可能為空的值文檔化,這比閱讀實現代碼要簡單很多。

下面我們舉例說明 Optional 對象的 API,從而切身體會一下它的使用方法。使用工廠方法 of,可以從某個值創建出一個 Optional 對象。Optional 對象相當於值的容器,而該值可以 通過 get 方法提取。

Optional<String> a = Optional.of("a"); 
assertEquals("a", a.get());

Optional 對象也可能為空,因此還有一個對應的工廠方法 empty,另外一個工廠方法 ofNullable 則可將一個空值轉換成 Optional 對象。下面的代碼同時展示 了第三個方法 isPresent 的用法(該方法表示一個 Optional 對象里是否有值)。

Optional emptyOptional = Optional.empty(); 
Optional alsoEmpty = Optional.ofNullable(null); assertFalse(emptyOptional.isPresent());

使用 Optional 對象的方式之一是在調用 get() 方法前,先使用 isPresent 檢查 Optional 對象是否有值。使用 orElse 方法則更簡潔,當 Optional 對象為空時,該方法提供了一個 備選值。如果計算備選值在計算上太過繁瑣,即可使用 orElseGet 方法。該方法接受一個 Supplier 對象,只有在 Optional 對象真正為空時才會調用。

assertEquals("b", emptyOptional.orElse("b")); 
assertEquals("c", emptyOptional.orElseGet(() -> "c"));

最後

實踐是檢驗真理的唯一標準,多寫代碼,多思考,你的代碼才會越來越好。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

網頁設計最專業,超強功能平台可客製化

您可能也會喜歡…