在本系列的前兩篇文章中,已經對函數式編程的思想和函數式編程的重要概念作了介紹。本文將介紹 Java 平臺自己對函數式編程的支持,着重介紹 Lambda 表達式和流(Stream)。html
當提到 Java 8 的時候,Lambda 表達式老是第一個提到的新特性。Lambda 表達式把函數式編程風格引入到了 Java 平臺上,能夠極大的提升 Java 開發人員的效率。這也是 Java 社區期待已久的功能,已經有不少的文章和圖書討論過 Lambda 表達式。本文則是基於官方的 JSR 335(Lambda Expressions for the Java Programming Language)來從另一個角度介紹 Lambda 表達式。java
咱們先從清單 1 中的代碼開始談起。該示例的功能很是簡單,只是啓動一個線程並輸出文本到控制檯。雖然該 Java 程序一共有 9 行代碼,但真正有價值的只有其中的第 5 行。剩下的代碼所有都是爲了知足語法要求而必須添加的冗餘代碼。代碼中的第 3 到第 7 行,使用 java.lang.Runnable 接口的實現建立了一個新的 java.lang.Thread 對象,並調用 Thread 對象的 start 方法來啓動它。Runnable 接口是經過一個匿名內部類實現的。express
public class OldThread { public static void main(String[] args) { new Thread(new Runnable() { public void run() { System.out.println("Hello World!"); } }).start(); }}
從簡化代碼的角度出發,第 3 行和第 7 行的 new Runnable() 能夠被刪除,由於接口類型 Runnable 能夠從類 Thread 的構造方法中推斷出來。第 4 和第 6 行一樣能夠被刪除,由於方法 run 是接口 Runnable 中的惟一方法。把第 5 行代碼做爲 run 方法的實現不會出現歧義。把第 3,4,6 和 7 行的代碼刪除掉以後,就獲得了使用 Lambda 表達式的實現方式,如清單 2 所示。只用一行代碼就完成了清單 1 中 5 行代碼完成的工做。這是使人興奮的變化。更少的代碼意味着更高的開發效率和更低的維護成本。這也是 Lambda 表達式深受歡迎的緣由。編程
public class LambdaThread { public static void main(String[] args) { new Thread(() -> System.out.println("Hello World!")).start(); }}
簡單來講,Lambda 表達式是建立匿名內部類的語法糖(syntax sugar)。在編譯器的幫助下,可讓開發人員用更少的代碼來完成工做。api
在對清單 1的代碼進行簡化時,咱們定義了兩個前提條件。第一個前提是要求接口類型,如示例中的 Runnable,能夠從當前上下文中推斷出來;第二個前提是要求接口中只有一個抽象方法。若是一個接口僅有一個抽象方法(除了來自 Object 的方法以外),它被稱爲函數式接口(functional interface)。函數式接口的特別之處在於其實例能夠經過 Lambda 表達式或方法引用來建立。Java 8 的 java.util.function 包中添加了不少新的函數式接口。若是一個接口被設計爲函數式接口,應該添加@FunctionalInterface 註解。編譯器會確保該接口確實是函數式接口。當嘗試往該接口中添加新的方法時,編譯器會報錯。數組
Lambda 表達式沒有類型信息。一個 Lambda 表達式的類型由編譯器根據其上下文環境在編譯時刻推斷得來。舉例來講,Lambda 表達式 () -> System.out.println("Hello World!") 能夠出如今任何要求一個函數式接口實例的上下文中,只要該函數式接口的惟一方法不接受任何參數,而且返回值是 void。這多是 Runnable 接口,也多是來自第三方庫或應用代碼的其餘函數式接口。由上下文環境所肯定的類型稱爲目標類型。Lambda 表達式在不一樣的上下文環境中能夠有不一樣的類型。相似 Lambda 表達式這樣,類型由目標類型肯定的表達式稱爲多態表達式(poly expression)。安全
Lambda 表達式的語法很靈活。它們的聲明方式相似 Java 中的方法,有形式參數列表和主體。參數的類型是可選的。在不指定類型時,由編譯器經過上下文環境來推斷。Lambda 表達式的主體能夠返回值或 void。返回值的類型必須與目標類型相匹配。當 Lambda 表達式的主體拋出異常時,異常的類型必須與目標類型的 throws 聲明相匹配。oracle
因爲 Lambda 表達式的類型由目標類型肯定,在可能出現歧義的狀況下,可能有多個類型知足要求,編譯器沒法獨自完成類型推斷。這個時候須要對代碼進行改寫,以幫助編譯器完成類型推斷。一個常見的作法是顯式地把 Lambda 表達式賦值給一個類型肯定的變量。另一種作法是顯示的指定類型。ide
在清單 3 中,函數式接口 A 和 B 分別有方法 a 和 b。兩個方法 a 和 b 的類型是相同的。類 UseAB 的 use 方法有兩個重載形式,分別接受類 A 和 B 的對象做爲參數。在方法 targetType 中,若是直接使用 () -> System.out.println("Use") 來調用 use 方法,會出現編譯錯誤。這是由於編譯器沒法推斷該 Lambda 表達式的類型,類型多是 A 或 B。這裏經過顯式的賦值操做爲 Lambda 表達式指定了類型 A,從而能夠編譯經過。函數式編程
public class LambdaTargetType { @FunctionalInterface interface A { void a(); } @FunctionalInterface interface B { void b(); } class UseAB { void use(A a) { System.out.println("Use A"); } void use(B b) { System.out.println("Use B"); } } void targetType() { UseAB useAB = new UseAB(); A a = () -> System.out.println("Use"); useAB.use(a); }}
在 Lambda 表達式的主體中,常常須要引用來自包圍它的上下文環境中的變量。Lambda 表達式使用一個簡單的策略來處理主體中的名稱解析問題。Lambda 表達式並無引入新的命名域(scope)。Lambda 表達式中的名稱與其所在上下文環境在同一個詞法域中。Lambda 表達式在執行時,就至關因而在包圍它的代碼中。在 Lambda 表達式中的 this 也與包圍它的代碼中的含義相同。在清單 4 中,Lambda 表達式的主體中引用了來自包圍它的上下文環境中的變量 name。
public void run() { String name = "Alex"; new Thread(() -> System.out.println("Hello, " + name)).start();}
須要注意的是,能夠在 Lambda 表達式中引用的變量必須是聲明爲 final 或是實際上 final(effectively final)的。實際上 final 的意思是變量雖然沒有聲明爲 final,可是在初始化以後沒有被賦值。所以變量的值沒有改變。
Java 8 中的流表示的是元素的序列。流中的元素多是對象、int、long 或 double 類型。流做爲一個高層次的抽象,並不關注流中元素的來源或是管理方式。流只關注對流中元素所進行的操做。當流與函數式接口和 Lambda 表達式一同使用時,能夠寫出簡潔高效的數據處理代碼。下面介紹幾個與流相關的基本概念。
流的操做能夠順序執行或並行執行, 後者能夠得到比前者更好的性能。可是若是實現不當,可能因爲數據競爭或無用的線程同步,致使並行執行時的性能更差。一個流是否會並行執行,能夠經過其方法 isParallel() 來判斷。根據流的建立方式,一個流有其默認的執行方式。可使用方法 sequential() 或 parallel() 來將其執行方式設置爲順序或並行。
一個流的相遇順序(encounter order)是流中的元素被處理時的順序。流根據其特徵可能有,也可能沒有一個肯定的相遇順序。舉例來講,從 ArrayList 建立的流有肯定的相遇順序;從 HashSet 建立的流沒有肯定的相遇順序。大部分的流操做會按照流的相遇順序來依次處理元素。若是一個流是無序的,同一個流處理流水線在屢次執行時可能產生不同的結果。好比 Stream 的 findFirst() 方法獲取到流中的第一個元素。若是在從 ArrayList 建立的流上應用該操做,返回的老是第一個元素;若是是從 HashSet 建立的流,則返回的結果是不肯定的。對於一個無序的流,可使用 sorted 操做來排序;對於一個有序的流,可使用 unordered() 方法來使其無序。
全部的流都是從 Spliterator 建立出來的。Spliterator 的名稱來源於它所支持的兩種操做:split 和 iterator。Spliterator 能夠當作是 Iterator 的並行版本,容許經過對流中元素分片的方式來切分數據源。使用其 tryAdvance 方法來順序遍歷元素,也可使用 trySplit 方法來建立一個新的 Spliterator 對象在新劃分的數據集上工做。Spliterator 還提供了 forEachRemaining 方法進行批量順序遍歷。可使用 estimateSize 方法來查詢可能會遍歷的元素數量。通常的作法是先使用 trySplit 切分數據源。當元素數量足夠小時,使用 forEachRemaining 來對分片中的所有元素進行處理。這也是典型的分治法的思路。
每一個 Spliterator 能夠有一系列不一樣的特徵,能夠經過 characteristics 方法來查詢。一個 Spliterator 具有的特徵取決於其數據源和元素。全部可用的特徵以下所示:
Spliterator 須要綁定到流以後才能遍歷其中的元素。不一樣的 Spliterator 實現可能有不一樣的綁定時機。若是一個 Spliterator 是延遲綁定的,那麼只有在進行首次遍歷、首次切分或首次查詢大小時,纔會綁定到流上;反之,它會在建立時或首次調用任何方法時綁定到流上。綁定時機的重要性在於,在綁定以前對流所作的修改,在 Spliterator 遍歷時是可見的。延遲綁定能夠提供最大限度的靈活性。
流操做能夠是有狀態或無狀態的。當一個有狀態的操做在處理一個元素時,它可能須要使用處理以前的元素時保留的信息;無狀態的操做能夠獨立處理每一個元素,舉例來講:
Stream<T> 是表示流的接口,T 是流中元素的類型。對於原始類型的流,可使用專門的類 IntStream、LongStream 和 DoubleStream。
在對流進行處理時,不一樣的流操做以級聯的方式造成處理流水線。一個流水線由一個源(source),0 到多箇中間操做(intermediate operation)和一個終結操做(terminal operation)完成。
流的處理流水線在其終結操做運行時纔開始執行。
Java 8 支持從不一樣的源中建立流。Stream.of 方法可使用給定的元素建立一個順序流。使用 java.util.Arrays 的靜態方法能夠從數組中建立流,如清單5 所示。
Arrays.stream(new String[] {"Hello", "World"}).forEach(System.out::println);// 輸出"Hello\nWorld"到控制檯 int sum = Arrays.stream(new int[] {1, 2, 3}).reduce((a, b) -> a + b).getAsInt();// "sum"的值是"6"
接口 Collection 的默認方法 stream() 和 parallelStream() 能夠分別從集合中建立順序流和並行流,如清單 6 所示。
List<String> list = new ArrayList<>();list.add("Hello");list.add("World");list.stream().forEach(System.out::println);// 輸出 Hello 和 World
流中間操做在應用到流上,返回一個新的流。下面列出了經常使用的流中間操做:
在清單 7 中,第一段代碼展現了 flatMap 的用法,第二段代碼展現了 takeWhile 和 dropWhile 的用法。
Stream.of(1, 2, 3) .map(v -> v + 1) .flatMap(v -> Stream.of(v * 5, v * 10)) .forEach(System.out::println);//輸出 10,20,15,30,20,40 Stream.of(1, 2, 3) .takeWhile(v -> v < 3) .dropWhile(v -> v < 2) .forEach(System.out::println);//輸出 2
終結操做產生最終的結果或反作用。下面是一些常見的終結操做。
forEach 和 forEachOrdered 對流中的每一個元素執行由 Consumer 給定的實現。在使用 forEach 時,並無肯定的處理元素的順序;forEachOrdered 則按照流的相遇順序來處理元素,若是流有肯定的相遇順序的話。
reduce 操做把一個流約簡成單個結果。約簡操做能夠有 3 個部分組成:
在清單 8 中,第一個 reduce 操做是最簡單的形式,只須要聲明疊加器便可。初始值是流的第一個元素;第二個 reduce 操做提供了初始值和疊加器;第三個 reduce 操做聲明瞭初始值、疊加器和合並器。
Stream.of(1, 2, 3).reduce((v1, v2) -> v1 + v2) .ifPresent(System.out::println);// 輸出 6 int result1 = Stream.of(1, 2, 3, 4, 5) .reduce(1, (v1, v2) -> v1 * v2);System.out.println(result1);// 輸出 120 int result2 = Stream.of(1, 2, 3, 4, 5) .parallel() .reduce(0, (v1, v2) -> v1 + v2, (v1, v2) -> v1 + v2);System.out.println(result2); // 輸出 15
Max 和 min 是兩種特殊的約簡操做,分別求得流中元素的最大值和最小值。
對於一個流,操做 allMatch、anyMatch 和 nonMatch 分別用來檢查是否流中的所有元素、任意元素或沒有元素知足給定的條件。判斷的條件由 Predicate 指定。
操做 findFirst 和 findAny 分別查找流中的第一個或任意一個元素。兩個方法的返回值都是 Optional 對象。當流爲空時,返回的是空的 Optional 對象。若是一個流沒有肯定的相遇順序,那麼 findFirst 和 findAny 的行爲在本質上是相同的。
操做 collect 表示的是另一類的約簡操做。與 reduce 不一樣在於,collect 會把結果收集到可變的容器中,如 List 或 Set。收集操做經過接口 java.util.stream.Collector 來實現。Java 已經在類 Collectors 中提供了不少經常使用的 Collector 實現。
第一類收集操做是收集到集合中,常見的方法有 toList()、toSet() 和 toMap() 等。第二類收集操做是分組收集,即便用 groupingBy 對流中元素進行分組。分組時對流中全部元素應用同一個 Function。具備相同結果的元素被分到同一組。分組以後的結果是一個 Map,Map 的鍵是應用 Function 以後的結果,而對應的值是屬於該組的全部元素的 List。在清單 9 中,流中的元素按照字符串的第一個字母分組,所獲得的 Map 中的鍵是 A、B 和 D,而 A 對應的 List 值中包含了 Alex 和 Amy 兩個元素,B 和 D 所對應的 List 值則只包含一個元素。
final Map<Character, List<String>> names = Stream.of("Alex", "Bob", "David", "Amy") .collect(Collectors.groupingBy(v -> v.charAt(0)));System.out.println(names);
第三類的 joining 操做只對元素類型爲 CharSequence 的流使用,其做用是把流中的字符串鏈接起來。清單 10 中把字符串流用", "進行鏈接。
String str = Stream.of("a", "b", "c") .collect(Collectors.joining(", "));System.out.println(str);
第四類的 partitioningBy 操做的做用相似於 groupingBy,只不過度組時使用的是 Predicate,也就是說元素最多分紅兩組。所獲得結果的 Map 的鍵的類型是 Boolean,而值的類型一樣是 List。
還有一些收集器能夠進行數學計算,不過只對元素類型爲 int、long 或 double 的流可用。這些數學計算包括:
清單 11 展現了這些數學計算相關的收集器的用法。
double avgLength = Stream.of("hello", "world", "a") .collect(Collectors.averagingInt(String::length));System.out.println(avgLength); final IntSummaryStatistics statistics = Stream.of("a", "b", "cd") .collect(Collectors.summarizingInt(String::length));System.out.println(statistics.getAverage());System.out.println(statistics.getCount());
Stream 中還有其餘實用的操做,限於篇幅不能所有介紹。相關的用法能夠查看 API 文檔。
Java 8 引入的 Lambda 表達式和流處理是能夠極大提升開發效率的重要特性。每一個 Java 開發人員都應該熟練掌握它們的使用。本文從 JSR 335 出發對 Lambda 表達式進行了深刻的介紹,同時也對流的特徵和操做進行了詳細說明。下一篇文章將對 Java 平臺上流行的函數式編程庫 Vavr 進行介紹。
原做者:成 富
原文連接: Java 8 的 Lambda 表達式和流處理
原出處:IBM Developer