重識Java8函數語言程式設計

東方雨傾發表於2020-06-21

前言

最近真的是太忙忙忙忙忙了,很久沒有更新文章了。最近工作中看到了幾段關於函數語言程式設計的程式碼,但是有點費解,於是就準備總結一下函數語言程式設計。很多東西很簡單,但是如果不總結,可能會被它的各種變體所困擾。接觸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"));

最後

實踐是檢驗真理的唯一標準,多寫程式碼,多思考,你的程式碼才會越來越好。
end
Java開發樂園

相關文章