Java 8 Strem基本操做

本文提供了有關Java 8 Stream的深刻概述。當我第一次讀到的Stream API,我感到很困惑,由於它聽起來相似Java I/O的InputStreamOutputStream。但Java 8 Stream是徹底不一樣的東西。Streams是Monads,所以在爲Java提供函數式編程方面發揮了重要做用:html

在函數式編程中,monad是表示定義爲步驟序列的計算的結構。具備monad結構的類型定義鏈操做的含義,或將該類型的函數嵌套在一塊兒。java

本文詳解如何使用Java 8 Stream以及如何使用不一樣類型的可用流操做。您將瞭解處理順序以及流操做的順序如何影響運行時性能。並對更強大的reducecollectflatMap流操做詳細介紹。編程

若是您還不熟悉Java 8 lambda表達式,函數接口和方法引用,那麼您可能須要瞭解Java 8。api

Stram如何工做


Stream表示一系列元素,並支持不一樣類型的操做以對這些元素執行計算:oracle

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

以上代碼的產出:less

C1
C2

Stream操做是中間操做或終端操做。中間操做返回一個流,所以咱們能夠連接多箇中間操做而不使用分號。終端操做無效或返回非流結果。在上述例子中filtermapsorted是中間操做,而forEach是一個終端的操做。有關全部可用流操做的完整列表,請參閱Stream Javadoc。如上例中所見的這種流操做鏈也稱爲操做管道。函數式編程

大多數流操做都接受某種lambda表達式參數,這是一個指定操做的確切行爲的功能接口。大多數這些操做必須是不受干擾和無狀態。函數

當函數不修改流的基礎數據源時,該函數是不受干擾的,例如在上面的示例中,沒有lambda表達式經過從集合中添加或刪除元素來修改streams。性能

當操做的執行是肯定性的時,函數是無狀態的,例如在上面的示例中,沒有lambda表達式依賴於任何可變變量或來自外部做用域的狀態,其可能在執行期間改變。優化

不一樣種類的Stream


能夠從各類數據源建立流,尤爲是集合。ListsSets支持新的方法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還附帶了特殊類型的流,用於處理原始數據類型intlong以及double。你可能已經猜到了IntStreamLongStream,DoubleStream

IntStreams可使用IntStream.range()方法替換常規for循環:

IntStream.range(1, 4)
    .forEach(System.out::println);

以上代碼的產出:

1
2
3

全部這些原始流都像常規對象流同樣工做,但有如下不一樣之處:原始流使用專門的lambda表達式,例如IntFunction代替FunctionIntPredicate代替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將輸入集合減小到只有一個元素。所以,對於較大的輸入集合,性能會大大提升。

重用Stream


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()構造一個咱們保存的新流,以調用所需的終端操做。

相關文章
相關標籤/搜索