Java 8 Strem基本操作
本文提供了有關Java 8 Stream的深入概述。當我第一次讀到的Stream API,我感到很困惑,因為它聽起來類似Java I/O的InputStream,OutputStream。但Java 8 Stream是完全不同的東西。Streams是Monads,因此在為Java提供函數語言程式設計方面發揮了重要作用:
在函數語言程式設計中,monad是表示定義為步驟序列的計算的結構。具有monad結構的型別定義鏈操作的含義,或將該型別的函式巢狀在一起。
本文詳解如何使用Java 8 Stream以及如何使用不同型別的可用流操作。您將瞭解處理順序以及流操作的順序如何影響執行時效能。並對更強大的reduce
如果您還不熟悉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操作是中間操作或終端操作。中間操作返回一個流,因此我們可以連結多箇中間操作而不使用分號。終端操作無效或返回非流結果。在上述例子中filter,map和sorted是中間操作,而forEach是一個終端的操作。有關所有可用流操作的完整列表,請參閱Stream Javadoc。如上例中所見的這種流操作鏈也稱為操作管道。
大多數流操作都接受某種lambda表示式引數,這是一個指定操作的確切行為的功能介面。大多數這些操作必須是不受干擾和無狀態。
當函式不修改流的基礎資料來源時,該函式是不受干擾的,例如在上面的示例中,沒有lambda表示式通過從集合中新增或刪除元素來修改streams。
當操作的執行是確定性的時,函式是無狀態的,例如在上面的示例中,沒有lambda表示式依賴於任何可變變數或來自外部作用域的狀態,其可能在執行期間改變。
不同種類的Stream
可以從各種資料來源建立流,尤其是集合。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將輸入集合減少到只有一個元素。因此,對於較大的輸入集合,效能會大大提高。
重用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()構造一個我們儲存的新流,以呼叫所需的終端操作。