JDK 8 函數式編程入門

1. 概述

1.1 函數式編程簡介

咱們最經常使用的面向對象編程(Java)屬於命令式編程(Imperative Programming)這種編程範式。常見的編程範式還有邏輯式編程(Logic Programming),函數式編程(Functional Programming)。java

函數式編程做爲一種編程範式,在科學領域,是一種編寫計算機程序數據結構和元素的方式,它把計算過程當作是數學函數的求值,而避免更改狀態和可變數據。git

函數式編程並不是近幾年的新技術或新思惟,距離它誕生已有大概50多年的時間了。它一直不是主流的編程思惟,但在衆多的所謂頂級編程高手的科學工做者間,函數式編程是十分盛行的。編程

什麼是函數式編程?簡單的回答:一切都是數學函數。函數式編程語言裏也能夠有對象,但一般這些對象都是恆定不變的 —— 要麼是函數參數,要什麼是函數返回值。函數式編程語言裏沒有 for/next 循環,由於這些邏輯意味着有狀態的改變。相替代的是,這種循環邏輯在函數式編程語言裏是經過遞歸、把函數當成參數傳遞的方式實現的。api

舉個例子:數據結構

a = a + 1

這段代碼在普通成員看來並無什麼問題,但在數學家看來確實不成立的,由於它意味着變量值得改變。閉包

1.2 Lambda 表達式簡介

Java 8的最大變化是引入了Lambda(Lambda 是希臘字母 λ 的英文名稱)表達式——一種緊湊的、傳遞行爲的方式。oracle

先看個例子:框架

button.addActionListener(new ActionListener() {
    public void actionPerformed(ActionEvent event) {
        System.out.println("button clicked");
    }
});

這段代碼使用了匿名類。ActionListener 是一個接口,這裏 new 了一個類實現了 ActionListener 接口,而後重寫了 actionPerformed 方法。actionPerformed 方法接收 ActionEvent 類型參數,返回空。編程語言

這段代碼咱們其實只關心中間打印的語句,其餘都是多餘的。因此使用 Lambda 表達式,咱們就能夠簡寫爲:

button.addActionListener(event -> System.out.println("button clicked"));

2. Lambda 表達式

2.1 Lambda 表達式的形式

Java 中 Lambda 表達式一共有五種基本形式,具體以下:

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

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

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

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

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

➊中所示的 Lambda 表達式不包含參數,使用空括號 () 表示沒有參數。該 Lambda 表達式 實現了 Runnable 接口,該接口也只有一個 run 方法,沒有參數,且返回類型爲 void。➋中所示的 Lambda 表達式包含且只包含一個參數,可省略參數的括號,這和例 2-2 中的 形式同樣。Lambda 表達式的主體不只能夠是一個表達式,並且也能夠是一段代碼塊,使用大括號 ({})將代碼塊括起來,如➌所示。該代碼塊和普通方法遵循的規則別無二致,能夠用返 回或拋出異常來退出。只有一行代碼的 Lambda 表達式也可以使用大括號,用以明確 Lambda表達式從何處開始、到哪裏結束。Lambda 表達式也能夠表示包含多個參數的方法,如➍所示。這時就有必要思考怎樣去閱 讀該 Lambda 表達式。這行代碼並非將兩個數字相加,而是建立了一個函數,用來計算 兩個數字相加的結果。變量 add 的類型是 BinaryOperator ,它不是兩個數字的和, 而是將兩個數字相加的那行代碼。到目前爲止,全部 Lambda 表達式中的參數類型都是由編譯器推斷得出的。這固然不錯, 但有時最好也能夠顯式聲明參數類型,此時就須要使用小括號將參數括起來,多個參數的 狀況也是如此。如➎所示。

記住一點很重要,Lambda 表達式均可以擴寫爲原始的「匿名類」形式。因此當你以爲這個 Lambda 表達式很複雜不容易理解的時候,不妨把它擴寫爲「匿名類」形式來看。

2.2 閉包

若是你之前使用過匿名內部類,也許遇到過這樣的問題。當你須要匿名內部類所在方法裏的變量,必須把該變量聲明爲 final。以下例子所示:

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

Java 8放鬆了這一限制,能夠沒必要再把變量聲明爲 final,但其實該變量實際上仍然是 final 的。雖然無需將變量聲明爲 final,但在 Lambda 表達式中,也沒法用做非終態變量。若是堅持用做非終態變量(即改變變量的值),編譯器就會報錯。

2.3 函數接口

上面例子裏提到了 ActionListener 接口,咱們看一下它的代碼:

public interface ActionListener extends EventListener {

    /**
     * Invoked when an action occurs.
     */
    public void actionPerformed(ActionEvent e);

}

ActionListener 只有一個抽象方法:actionPerformed,被用來表示行爲:接受一個參數,返回空。記住,因爲 actionPerformed 定義在一個接口裏,所以 abstract 關鍵字不是必需的。該接口也繼承自一個不具備任何方法的父接口:EventListener

咱們把這種接口就叫作函數接口。

JDK 8 中提供了一組經常使用的核心函數接口:

接口 參數 返回類型 描述
Predicate<T> T boolean 用於判別一個對象。好比求一我的是否爲男性
Consumer<T> T void 用於接收一個對象進行處理但沒有返回,好比接收一我的並打印他的名字
Function<T, R> T R 轉換一個對象爲不一樣類型的對象
Supplier<T> None T 提供一個對象
UnaryOperator<T> T T 接收對象並返回同類型的對象
BinaryOperator<T> (T, T) T 接收兩個同類型的對象,並返回一個原類型對象

其中 CosumerSupplier 對應,一個是消費者,一個是提供者。

Predicate 用於判斷對象是否符合某個條件,常常被用來過濾對象。

Function 是將一個對象轉換爲另外一個對象,好比說要裝箱或者拆箱某個對象。

UnaryOperator 接收和返回同類型對象,通常用於對對象修改屬性。BinaryOperator 則能夠理解爲合併對象。

若是之前接觸過一些其餘 Java 框架,好比 Google Guava,可能已經使用過這些接口,對這些東西並不陌生。因此,其實 Java 8 的改進並非閉門造車,而是集百家之長。

3. 集合處理

3.1 Stream 簡介

在程序編寫過程當中,集合的處理應該是很廣泛的。Java 8 對於 Collection 的處理花了很大的功夫,若是從 JDK 7 過渡到 JDK 8,這一塊也多是咱們感覺最爲明顯的。

Java 8 中,引入了流(Stream)的概念,這個流和之前咱們使用的 IO 中的流並不太相同。

全部繼承自 Collection 的接口均可以轉換爲 Stream。仍是看一個例子。

假設咱們有一個 List 包含一系列的 PersonPerson 有姓名 name 和年齡 age 連個字段。現要求這個列表中年齡大於 20 的人數。

一般按照之前咱們可能會這麼寫:

long count = 0;
for (Person p : persons) {
    if (p.getAge() > 20) {
        count ++;
    }
}

但若是使用 stream 的話,則會簡單不少:

long count = persons.stream()
                    .filter(person -> person.getAge() > 20)
                    .count();

這只是 stream 的很簡單的一個用法。如今鏈式調用方法算是一個主流,這樣寫也更利於閱讀和理解編寫者的意圖,一步方法作一件事。

3.2 Stream 經常使用操做

Stream 的方法分爲兩類。一類叫惰性求值,一類叫及早求值。

判斷一個操做是惰性求值仍是及早求值很簡單:只需看它的返回值。若是返回值是 Stream,那麼是惰性求值。其實能夠這麼理解,若是調用惰性求值方法,Stream 只是記錄下了這個惰性求值方法的過程,並無去計算,等到調用及早求值方法後,就連同前面的一系列惰性求值方法順序進行計算,返回結果。

通用形式爲:

Stream.惰性求值.惰性求值. ... .惰性求值.及早求值

整個過程和建造者模式有共通之處。建造者模式使用一系列操做設置屬性和配置,最後調 用一個 build 方法,這時,對象才被真正建立。

3.2.1 collect(toList())

collect(toList()) 方法由 Stream 裏的值生成一個列表,是一個及早求值操做。能夠理解爲 StreamCollection 的轉換。

注意這邊的 toList() 實際上是 Collectors.toList(),由於採用了靜態倒入,看起來顯得簡潔。

List<String> collected = Stream.of("a", "b", "c")
                               .collect(Collectors.toList());
assertEquals(Arrays.asList("a", "b", "c"), collected);

3.2.2 map

若是有一個函數能夠將一種類型的值轉換成另一種類型,map 操做就可使用該函數,將一個流中的值轉換成一個新的流。

List<String> collected = Stream.of("a", "b", "hello")
                               .map(string -> string.toUpperCase())
                               .collect(toList());
assertEquals(asList("A", "B", "HELLO"), collected);

map 方法就是接受的一個 Function 的匿名函數類,進行的轉換。

3.2.3 filter

遍歷數據並檢查其中的元素時,可嘗試使用 Stream 中提供的新方法 filter

List<String> beginningWithNumbers = 
        Stream.of("a", "1abc", "abc1")
              .filter(value -> isDigit(value.charAt(0)))
              .collect(toList());
assertEquals(asList("1abc"), beginningWithNumbers);

filter 方法就是接受的一個 Predicate 的匿名函數類,判斷對象是否符合條件,符合條件的才保留下來。

3.2.4 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);

flatMap 最經常使用的操做就是合併多個 Collection

3.2.5 max和min

Stream 上經常使用的操做之一是求最大值和最小值。Stream API 中的 maxmin 操做足以解決這一問題。

List<Integer> list = Lists.newArrayList(3, 5, 2, 9, 1);
int maxInt = list.stream()
                 .max(Integer::compareTo)
                 .get();
int minInt = list.stream()
                 .min(Integer::compareTo)
                 .get();
assertEquals(maxInt, 9);
assertEquals(minInt, 1);

這裏有 2 個要點須要注意:

  1. maxmin 方法返回的是一個 Optional 對象(對了,和 Google Guava 裏的 Optional 對象是同樣的)。Optional 對象封裝的就是實際的值,可能爲空,因此保險起見,能夠先用 isPresent() 方法判斷一下。Optional 的引入就是爲了解決方法返回 null 的問題。
  2. Integer::compareTo 也是屬於 Java 8 引入的新特性,叫作 方法引用(Method References)。在這邊,其實就是 (int1, int2) -> int1.compareTo(int2) 的簡寫,能夠本身查閱瞭解,這裏再也不多作贅述。

3.2.6 reduce

reduce 操做能夠實現從一組值中生成一個值。在上述例子中用到的 countminmax 方法,由於經常使用而被歸入標準庫中。事實上,這些方法都是 reduce 操做。

上圖展現了 reduce 進行累加的一個過程。具體的代碼以下:

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

注意 reduce 的第一個參數,這是一個初始值。0 + 1 + 2 + 3 + 4 = 10

若是是累乘,則爲:

int result = Stream.of(1, 2, 3, 4)
                   .reduce(1, (acc, element) -> acc * element);
assertEquals(24, result);

由於任何數乘以 1 都爲其自身嘛。1 * 1 * 2 * 3 * 4 = 24

Stream 的方法還有不少,這裏列出的幾種都是比較經常使用的。Stream 還有不少通用方法,具體能夠查閱 Java 8 的 API 文檔。

https://docs.oracle.com/javase/8/docs/api/

3.3 數據並行化操做

Stream 的並行化也是 Java 8 的一大亮點。數據並行化是指將數據分紅塊,爲每塊數據分配單獨的處理單元。這樣能夠充分利用多核 CPU 的優點。

並行化操做流只需改變一個方法調用。若是已經有一個 Stream 對象,調用它的 parallel() 方法就能讓其擁有並行操做的能力。若是想從一個集合類建立一個流,調用 parallelStream() 就能當即得到一個擁有並行能力的流。

int sumSize = Stream.of("Apple", "Banana", "Orange", "Pear")
                    .parallel()
                    .map(s -> s.length())
                    .reduce(Integer::sum)
                    .get();
assertEquals(sumSize, 21);

這裏求的是一個字符串列表中各個字符串長度總和。

若是你去計算這段代碼所花的時間,極可能比不加上 parallel() 方法花的時間更長。這是由於數據並行化會先對數據進行分塊,而後對每塊數據開闢線程進行運算,這些地方會花費額外的時間。並行化操做只有在 數據規模比較大 或者 數據的處理時間比較長 的時候才能體現出有事,因此並非每一個地方都須要讓數據並行化,應該具體問題具體分析。

3.4 其餘

3.4.1 收集器

Stream 轉換爲 List 是很經常使用的操做,其餘 Collectors 還有不少方法,能夠將 Stream 轉換爲 Set, 或者將數據分組並轉換爲 Map,並對數據進行處理。也能夠指定轉換爲具體類型,如 ArrayList, LinkedList 或者 HashMap。甚至能夠自定義 Collectors,編寫本身的收集器。

Collectors (收集器)的內容太多,有興趣的能夠本身研究。

http://my.oschina.net/joshuashaw/blog/487322
https://docs.oracle.com/javase/8/docs/api/java/util/stream/Collectors.html

3.4.2 元素順序

另一個還沒有說起的關於集合類的內容是流中的元素以何種順序排列。一些集合類型中的元素是按順序排列的,好比 List;而另外一些則是無序的,好比 HashSet。增長了流操做後,順序問題變得更加複雜。

總之記住。若是集合自己就是無序的,由今生成的流也是無序的。一些中間操做會產生順序,好比對值作映射時,映射後的值是有序的,這種順序就會保留 下來。若是進來的流是無序的,出去的流也是無序的。

若是咱們須要對流中的數據進行排序,能夠調用 sorted 方法:

List<Integer> list = Lists.newArrayList(3, 5, 1, 10, 8);
List<Integer> sortedList = list.stream()
                               .sorted(Integer::compareTo)
                               .collect(Collectors.toList());
assertEquals(sortedList, Lists.newArrayList(1, 3, 5, 8, 10));

3.4.3 @FunctionalInterface

咱們討論過函數接口定義的標準,但未說起 @FunctionalInterface 註釋。事實上,每一個用做函數接口的接口都應該添加這個註釋。

但 Java 中有一些接口,雖然只含一個方法,但並非爲了使用 Lambda 表達式來實現的。好比,有些對象內部可能保存着某種狀態,使用帶有一個方法的接口可能純屬巧合。

該註釋會強制 javac 檢查一個接口是否符合函數接口的標準。若是該註釋添加給一個枚舉類型、類或另外一個註釋,或者接口包含不止一個抽象方法,javac 就會報錯。重構代碼時,使用它能很容易發現問題。

參考

  1. 【Java 8 函數式編程】by Richard Warburton
  2. OpenJDK
  3. 函數式編程初探 by 阮一峯
  4. 函數式編程是一個倒退
相關文章
相關標籤/搜索