本文提供了有關Java 8 Stream的深刻概述。當我第一次讀到的Stream API,我感到很困惑,由於它聽起來相似Java I/O的InputStream,OutputStream。但Java 8 Stream是徹底不一樣的東西。Streams是Monads,所以在爲Java提供函數式編程方面發揮了重要做用:html
在函數式編程中,monad是表示定義爲步驟序列的計算的結構。具備monad結構的類型定義鏈操做的含義,或將該類型的函數嵌套在一塊兒。
本文詳解如何使用Java 8 Stream以及如何使用不一樣類型的可用流操做。您將瞭解處理順序以及流操做的順序如何影響運行時性能。並對更強大的reduce,collect,flatMap流操做詳細介紹。java
若是您還不熟悉Java 8 lambda表達式,函數接口和方法引用,那麼您可能須要瞭解Java 8。編程
Stream表示一系列元素,並支持不一樣類型的操做以對這些元素執行計算:api
List<String> streams = Arrays.asList("a1", "a2", "b1", "c2", "c1"); streams .stream() .filter(s -> s.startsWith("c")) .map(String::toUpperCase) .sorted() .forEach(System.out::println);
以上代碼的產出:oracle
C1 C2
Stream操做是中間操做或終端操做。中間操做返回一個流,所以咱們能夠連接多箇中間操做而不使用分號。終端操做無效或返回非流結果。在上述例子中filter,map和sorted是中間操做,而forEach是一個終端的操做。有關全部可用流操做的完整列表,請參閱Stream Javadoc。如上例中所見的這種流操做鏈也稱爲操做管道。less
大多數流操做都接受某種lambda表達式參數,這是一個指定操做的確切行爲的功能接口。大多數這些操做必須是不受干擾和無狀態。函數式編程
當函數不修改流的基礎數據源時,該函數是不受干擾的,例如在上面的示例中,沒有lambda表達式經過從集合中添加或刪除元素來修改streams。函數
當操做的執行是肯定性的時,函數是無狀態的,例如在上面的示例中,沒有lambda表達式依賴於任何可變變量或來自外部做用域的狀態,其可能在執行期間改變。性能
能夠從各類數據源建立流,尤爲是集合。Lists和Sets支持新的方法stream()
和parallelStream()
來建立順序流或並行流。並行流可以在多個線程上操做,後面的部分將對此進行介紹。咱們如今關注的是順序流:優化
Arrays.asList("a1", "a2", "a3") .stream() .findFirst() .ifPresent(System.out::println);
以上代碼的產出:
a1
在對象列表上調用stream()
方法將返回常規對象流。可是咱們沒必要建立集合以便使用流,就像咱們在下一個代碼示例中看到的那樣:
Stream.of("a1", "a2", "a3") .findFirst() .ifPresent(System.out::println);
以上代碼的產出:
a1
只是用來Stream.of()
從一堆對象引用建立一個流。
除了常規對象流以外,Java 8還附帶了特殊類型的流,用於處理原始數據類型int,long以及double。你可能已經猜到了IntStream
,LongStream
,DoubleStream
。
IntStreams可使用IntStream.range()
方法替換常規for循環:
IntStream.range(1, 4) .forEach(System.out::println);
以上代碼的產出:
1 2 3
全部這些原始流都像常規對象流同樣工做,但有如下不一樣之處:原始流使用專門的lambda表達式,例如IntFunction代替Function或IntPredicate代替Predicate。原始流支持額外的終端聚合操做,sum()
,average()
:
Arrays.stream(new int[] {1, 2, 3}) .map(n -> 2 * n + 1) .average() .ifPresent(System.out::println);
以上代碼的產出:
5.0
有時將常規對象流轉換爲基本流是有用的,反之亦然。爲此,對象流支持特殊的映射操做mapToInt()
,mapToLong()
,mapToDouble
:
Stream.of("a1", "a2", "a3") .map(s -> s.substring(1)) .mapToInt(Integer::parseInt) .max() .ifPresent(System.out::println);
以上代碼的產出:
3
能夠經過mapToObj()
方式將原始流轉換爲對象流:
IntStream.range(1, 4) .mapToObj(i -> "a" + i) .forEach(System.out::println);
以上代碼的產出:
a1 a2 a3
下面是一個組合示例:雙精度流首先映射到int流,而後映射到字符串的對象流:
Stream.of(1.0, 2.0, 3.0) .mapToInt(Double::intValue) .mapToObj(i -> "a" + i) .forEach(System.out::println);
以上代碼的產出:
a1 a2 a3
如今咱們已經學會了如何建立和使用不一樣類型的流,讓咱們深刻了解如何在流程下處理流操做。
中間操做的一個重要特徵是懶惰。查看缺乏終端操做的示例:
Stream.of("d2", "a2", "b1", "b3", "c") .filter(s -> { System.out.println("filter: " + s); return true; });
執行此代碼段時,不會向控制檯打印任何內容。這是由於只有在存在終端操做時才執行中間操做。
讓咱們經過forEach
終端操做擴展上面的例子:
Stream.of("d2", "a2", "b1", "b3", "c") .filter(s -> { System.out.println("filter: " + s); return true; }) .forEach(s -> System.out.println("forEach: " + s));
執行此代碼段會在控制檯上產生所需的輸出:
filter: d2 forEach: d2 filter: a2 forEach: a2 filter: b1 forEach: b1 filter: b3 forEach: b3 filter: c forEach: c
結果的順序可能會使人驚訝。默認認爲是在流的全部元素上一個接一個地水平執行操做。但相反,每一個元素都沿着鏈垂直移動。第一個字符串「d2」經過filter,而後forEach,而後處理第二個字符串「a2」。
此行爲能夠減小對每一個元素執行的實際操做數,以下一個示例所示:
Stream.of("d2", "a2", "b1", "b3", "c") .map(s -> { System.out.println("map: " + s); return s.toUpperCase(); }) .anyMatch(s -> { System.out.println("anyMatch: " + s); return s.startsWith("A"); });
代碼產出
map: d2 anyMatch: D2 map: a2 anyMatch: A2
一旦謂詞應用於給定的輸入元素,anyMatch
操做將返回true。這對於傳遞給「A2」的第二個元素是正確的。因爲流鏈的垂直執行,map
在這種狀況下映射只需執行兩次。所以,不是映射流的全部元素,而是map
儘量少地調用。
下一個示例包括兩個map
,filter
中間操做和forEach
終端操做。讓咱們再次檢查這些操做是如何執行的:
Stream.of("d2", "a2", "b1", "b3", "c") .map(s -> { System.out.println("map: " + s); return s.toUpperCase(); }) .filter(s -> { System.out.println("filter: " + s); return s.startsWith("A"); }) .forEach(s -> System.out.println("forEach: " + s));
代碼產出:
map: d2 filter: D2 map: a2 filter: A2 forEach: A2 map: b1 filter: B1 map: b3 filter: B3 map: c filter: C
正如您可能已經猜到的,對於底層集合中的每一個字符串,map和filter都被調用5次,而forEach只被調用一次。
若是咱們改變操做的順序,移動filter到鏈的開頭,咱們能夠大大減小實際的執行次數:
Stream.of("d2", "a2", "b1", "b3", "c") .filter(s -> { System.out.println("filter: " + s); return s.startsWith("a"); }) .map(s -> { System.out.println("map: " + s); return s.toUpperCase(); }) .forEach(s -> System.out.println("forEach: " + s));
代碼產出:
filter: d2 filter: a2 map: a2 forEach: A2 filter: b1 filter: b3 filter: c
如今,map只調用一次,所以操做管道對大量輸入元素的執行速度要快得多。在編寫複雜的方法鏈時要記住這一點。
讓咱們經過一個sorted
額外的操做來擴展上面的例子:
Stream.of("d2", "a2", "b1", "b3", "c") .sorted((s1, s2) -> { System.out.printf("sort: %s; %s\n", s1, s2); return s1.compareTo(s2); }) .filter(s -> { System.out.println("filter: " + s); return s.startsWith("a"); }) .map(s -> { System.out.println("map: " + s); return s.toUpperCase(); }) .forEach(s -> System.out.println("forEach: " + s));
排序是一種特殊的中間操做。這是一個所謂的有狀態操做,由於爲了對在排序期間必須維護狀態的元素集合進行排序。
執行此示例將致使如下控制檯輸出:
sort: a2; d2 sort: b1; a2 sort: b1; d2 sort: b1; a2 sort: b3; b1 sort: b3; d2 sort: c; b3 sort: c; d2 filter: a2 map: a2 forEach: A2 filter: b1 filter: b3 filter: c filter: d2
首先,對整個輸入集合執行排序操做。換句話說,sorted是水平執行的。所以,在這種狀況下sorted,對輸入集合中的每一個元素的多個組合調用八次。
咱們能夠經過從新排序鏈來優化性能:
Stream.of("d2", "a2", "b1", "b3", "c") .filter(s -> { System.out.println("filter: " + s); return s.startsWith("a"); }) .sorted((s1, s2) -> { System.out.printf("sort: %s; %s\n", s1, s2); return s1.compareTo(s2); }) .map(s -> { System.out.println("map: " + s); return s.toUpperCase(); }) .forEach(s -> System.out.println("forEach: " + s));
代碼產出
filter: d2 filter: a2 filter: b1 filter: b3 filter: c map: a2 forEach: A2
在此示例sorted從未被調用過,由於filter將輸入集合減小到只有一個元素。所以,對於較大的輸入集合,性能會大大提升。
Java 8 Stream沒法重用。只要您調用任何終端操做,流就會關閉:
Stream<String> stream = Stream.of("d2", "a2", "b1", "b3", "c") .filter(s -> s.startsWith("a")); stream.anyMatch(s -> true); // ok stream.noneMatch(s -> true); // exception
在同一流上的anyMatch
以後調用noneMatch
會致使如下異常:
java.lang.IllegalStateException: stream has already been operated upon or closed at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:229) at java.util.stream.ReferencePipeline.noneMatch(ReferencePipeline.java:459) at com.winterbe.java8.Streams5.test7(Streams5.java:38) at com.winterbe.java8.Streams5.main(Streams5.java:28)
爲了克服這個限制,咱們必須爲咱們想要執行的每一個終端操做建立一個新的流鏈,例如咱們能夠建立一個流供應商來構建一個新的流,其中已經設置了全部中間操做:
Supplier<Stream<String>> streamSupplier = () -> Stream.of("d2", "a2", "b1", "b3", "c") .filter(s -> s.startsWith("a")); streamSupplier.get().anyMatch(s -> true); // ok streamSupplier.get().noneMatch(s -> true); // ok
每次調用get()構造一個咱們保存的新流,以調用所需的終端操做。