1. 程式人生 > >Java 8 Strem基本操作

Java 8 Strem基本操作

本文提供了有關Java 8 Stream的深入概述。當我第一次讀到的Stream API,我感到很困惑,因為它聽起來類似Java I/O的InputStreamOutputStream。但Java 8 Stream是完全不同的東西。Streams是Monads,因此在為Java提供函數語言程式設計方面發揮了重要作用:

在函數語言程式設計中,monad是表示定義為步驟序列的計算的結構。具有monad結構的型別定義鏈操作的含義,或將該型別的函式巢狀在一起。

本文詳解如何使用Java 8 Stream以及如何使用不同型別的可用流操作。您將瞭解處理順序以及流操作的順序如何影響執行時效能。並對更強大的reduce

collectflatMap流操作詳細介紹。

如果您還不熟悉Java 8 lambda表示式,函式介面和方法引用,那麼您可能需要了解Java 8。

Stram如何工作


Stream表示一系列元素,並支援不同型別的操作以對這些元素執行計算:

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);
複製程式碼

以上程式碼的產出:

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()構造一個我們儲存的新流,以呼叫所需的終端操作。