在上一篇博客中,我介紹了構建 Stream 流的多種方式,以及 Stream 流的特點和優勢。如果你還沒有閱讀,你可以點擊這里查看。
Java基礎 - Stream 流:構建流的多種方式
在這篇博客中,我將探索 Stream API 的中間操作,它們可以讓你對 Stream 流進行各種轉換和過濾,從而得到你想要的結果。
Stream API 的中間操作是指那些返回一個新的 Stream 流對象的操作,它們不會消耗 Stream 流,也不會產生最終的結果,而是可以鏈式地調用,形成一個操作管道。Stream API 提供了很多中間操作,可以分為以下幾類:
- 篩選和切片:這類操作可以讓你從 Stream 流中選擇或者排除一些元素,例如
filter
,distinct
,limit
,skip
等。 - 映射:這類操作可以讓你將 Stream 流中的每個元素轉換為另一種類型或者形式,例如
map
,flatMap
,peek
等。 - 排序:這類操作可以讓你對 Stream 流中的元素進行排序,例如
sorted
,reverseOrder
等。 - 搜索:這類操作可以讓你在 Stream 流中查找某些元素或者條件,例如
findAny
,findFirst
,anyMatch
,allMatch
,noneMatch
等。 - 截斷:這類操作可以讓你在 Stream 流中截取某些元素或者條件,例如
takeWhile
,dropWhile
等。
下面,我將用一些示例來展示這些中間操作的用法和效果。
1. 篩選和切片
1.1 filter
filter
操作可以讓你根據一個謂詞(Predicate)來篩選出 Stream 流中符合條件的元素,返回一個新的 Stream 流對象。例如:
// 創建一個 Stream 流對象
Stream<Integer> numberStream = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);// 使用 filter 操作篩選出偶數
Stream<Integer> evenStream = numberStream.filter(n -> n % 2 == 0);// 輸出 [2, 4, 6, 8, 10]
evenStream.forEach(System.out::println);
1.2 distinct
distinct
操作可以讓你去除 Stream 流中的重復元素,返回一個新的 Stream 流對象。例如:
// 創建一個 Stream 流對象
Stream<String> animalStream = Stream.of("cat", "dog", "cat", "elephant", "dog", "fox");// 使用 distinct 操作去除重復元素
Stream<String> uniqueStream = animalStream.distinct();// 輸出 [cat, dog, elephant, fox]
uniqueStream.forEach(System.out::println);
1.3 limit
limit
操作可以讓你限制 Stream 流中的元素個數,返回一個新的 Stream 流對象。例如:
// 創建一個 Stream 流對象
Stream<String> fruitStream = Stream.of("apple", "banana", "cherry", "durian", "elderberry", "fig");// 使用 limit 操作限制元素個數為 3
Stream<String> limitedStream = fruitStream.limit(3);// 輸出 [apple, banana, cherry]
limitedStream.forEach(System.out::println);
1.4 skip
skip
操作可以讓你跳過 Stream 流中的前 n 個元素,返回一個新的 Stream 流對象。例如:
// 創建一個 Stream 流對象
Stream<String> colorStream = Stream.of("red", "green", "blue", "yellow", "pink", "purple");// 使用 skip 操作跳過前 2 個元素
Stream<String> skippedStream = colorStream.skip(2);// 輸出 [blue, yellow, pink, purple]
skippedStream.forEach(System.out::println);
2. 映射
2.1 map
map
操作可以讓你將 Stream 流中的每個元素映射為另一種類型或者形式,返回一個新的 Stream 流對象。例如:
// 創建一個 Stream 流對象
Stream<String> nameStream = Stream.of("Alice", "Bob", "Charlie", "David");// 使用 map 操作將每個元素轉換為大寫
Stream<String> upperStream = nameStream.map(String::toUpperCase);// 輸出 [ALICE, BOB, CHARLIE, DAVID]
upperStream.forEach(System.out::println);
2.2 flatMap
flatMap
操作可以讓你將 Stream 流中的每個元素映射為另一個 Stream 流對象,然后將這些 Stream 流對象合并為一個 Stream 流對象,返回一個新的 Stream 流對象。例如:
// 創建一個 Stream 流對象
Stream<String> sentenceStream = Stream.of("Hello world", "Java 8", "Stream API");// 使用 flatMap 操作將每個元素分割為單詞,并合并為一個 Stream 流對象
Stream<String> wordStream = sentenceStream.flatMap(s -> Stream.of(s.split(" ")));// 輸出 [Hello, world, Java, 8, Stream, API]
wordStream.forEach(System.out::println);
2.3 peek
peek
操作可以讓你在 Stream 流中的每個元素上執行一個消費者(Consumer)操作,同時返回一個新的 Stream 流對象,它和原來的 Stream 流對象包含相同的元素。這個操作通常用于調試或者觀察 Stream 流的中間狀態。例如:
// 創建一個 Stream 流對象
Stream<Integer> numberStream = Stream.of(1, 2, 3, 4, 5);// 使用 peek 操作在每個元素上打印一個消息
Stream<Integer> peekedStream = numberStream.peek(n -> System.out.println("Processing " + n));// 輸出 [1, 2, 3, 4, 5]
peekedStream.forEach(System.out::println);
3. 排序
3.1 sorted
sorted
操作可以讓你對 Stream 流中的元素進行自然排序或者指定排序規則,返回一個新的 Stream 流對象。例如:
// 創建一個 Stream 流對象
Stream<String> animalStream = Stream.of("cat", "dog", "elephant", "fox", "giraffe");// 使用 sorted 操作對元素進行自然排序
Stream<String> sortedStream = animalStream.sorted();// 輸出 [cat, dog, elephant, fox, giraffe]
sortedStream.forEach(System.out::println);// 使用 sorted 操作對元素進行指定排序規則,按照長度逆序
Stream<String> reversedStream = animalStream.sorted((s1, s2) -> s2.length() - s1.length());// 輸出 [elephant, giraffe, cat, dog, fox]
reversedStream.forEach(System.out::println);
3.2 reverseOrder
reverseOrder
操作可以讓你對 Stream 流中的元素進行逆序排序,返回一個新的 Stream 流對象。例如:
// 創建一個 Stream 流對象
Stream<Integer> numberStream = Stream.of(1, 2, 3, 4, 5);// 使用 reverseOrder 操作對元素進行逆序排序
Stream<Integer> reversedStream = numberStream.reverseOrder();// 輸出 [5, 4, 3, 2, 1]
reversedStream.forEach(System.out::println);
4. 搜索
4.1 findAny
findAny
操作可以讓你從 Stream 流中找到任意一個元素,返回一個 Optional 對象,它可能包含一個值,也可能為空。這個操作通常用于并行的 Stream 流,因為它不保證返回第一個元素。例如:
// 創建一個 Stream 流對象
Stream<String> animalStream = Stream.of("cat", "dog", "elephant", "fox", "giraffe");// 使用 findAny 操作找到任意一個元素
Optional<String> anyAnimal = animalStream.findAny();// 輸出 Optional[cat] 或者其他值
System.out.println(anyAnimal);
4.2 findFirst
findFirst
操作可以讓你從 Stream 流中找到第一個元素,返回一個 Optional 對象,它可能包含一個值,也可能為空。這個操作通常用于串行的 Stream 流,因為它保證返回第一個元素。例如:
// 創建一個 Stream 流對象
Stream<String> animalStream = Stream.of("cat", "dog", "elephant", "fox", "giraffe");// 使用 findFirst 操作找到第一個元素
Optional<String> firstAnimal = animalStream.findFirst();// 輸出 Optional[cat]
System.out.println(firstAnimal);
4.3 anyMatch
anyMatch
操作可以讓你判斷 Stream 流中是否有任意一個元素滿足一個謂詞(Predicate),返回一個布爾值。例如:
// 創建一個 Stream 流對象
Stream<String> animalStream = Stream.of("cat", "dog", "elephant", "fox", "giraffe");// 使用 anyMatch 操作判斷是否有以 f 開頭的元素
boolean hasF = animalStream.anyMatch(s -> s.startsWith("f"));// 輸出 true
System.out.println(hasF);
4.4 allMatch
allMatch
操作可以讓你判斷 Stream 流中是否所有的元素都滿足一個謂詞(Predicate),返回一個布爾值。例如:
// 創建一個 Stream 流對象
Stream<String> animalStream = Stream.of("cat", "dog", "elephant", "fox", "giraffe");// 使用 allMatch 操作判斷是否所有的元素都包含 a
boolean allA = animalStream.allMatch(s -> s.contains("a"));// 輸出 false
System.out.println(allA);
4.5 noneMatch
noneMatch
操作可以讓你判斷 Stream 流中是否沒有任何一個元素滿足一個謂詞(Predicate),返回一個布爾值。例如:
// 創建一個 Stream 流對象
Stream<String> animalStream = Stream.of("cat", "dog", "elephant", "fox", "giraffe");// 使用 noneMatch 操作判斷是否沒有以 z 結尾的元素
boolean noZ = animalStream.noneMatch(s -> s.endsWith("z"));// 輸出 true
System.out.println(noZ);
5. 截斷
5.1 takeWhile
takeWhile
操作可以讓你從 Stream 流中截取滿足一個謂詞(Predicate)的元素,直到遇到第一個不滿足的元素為止,返回一個新的 Stream 流對象。例如:
// 創建一個 Stream 流對象
Stream<Integer> numberStream = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);// 使用 takeWhile 操作截取小于 5 的元素
Stream<Integer> takenStream = numberStream.takeWhile(n -> n < 5);// 輸出 [1, 2, 3, 4]
takenStream.forEach(System.out::println);
5.2 dropWhile
dropWhile
操作可以讓你從 Stream 流中丟棄滿足一個謂詞(Predicate)的元素,直到遇到第一個不滿足的元素為止,然后返回剩余的元素組成的新的 Stream 流對象。例如:
// 創建一個 Stream 流對象
Stream<Integer> numberStream = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);// 使用 dropWhile 操作丟棄小于 5 的元素
Stream<Integer> droppedStream = numberStream.dropWhile(n -> n < 5);// 輸出 [5, 6, 7, 8, 9, 10]
droppedStream.forEach(System.out::println);
6. 注意事項
6.1 只能被消費一次
一個Steam只能被消費(執行終端操作)一次。如果重復進行消費則會拋出IllegalStateException
。
// 創建一個 Stream 流對象
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);// 正常執行
long count = wordStream.count();// 會拋出IllegalStateException
long anotherCount = wordStream.count();
6.2 并行流謹慎使用
Stream API 提供了并行流的支持,可以通過 parallel()
方法將順序流轉換為并行流。并行流是指可以利用多核處理器的優勢,將一個流的元素分成多個數據塊,然后用不同的線程并行地處理每個數據塊的流。使用并行流可以提高性能,但也有一些注意事項。
- 并行流不一定總是比順序流快:并行流的性能受到很多因素的影響,比如數據源的可分割性,流的操作的計算成本,流的大小,硬件環境等。并行流會引入額外的開銷,比如線程的分配和切換,數據的拆分和合并等。因此,并行流不適合處理小量的數據,或者涉及裝箱拆箱,依賴前一個元素,有狀態的操作等。在使用并行流之前,最好先進行性能測試,比較并行流和順序流的效果,然后選擇合適的方式。
- 并行流需要考慮線程安全問題:并行流會使用默認的 ForkJoinPool 線程池來執行任務,這個線程池的大小默認是 CPU 的核心數,也可以通過設置系統屬性
java.util.concurrent.ForkJoinPool.common.parallelism
來改變。如果并行流的操作涉及到共享變量的修改,或者調用了其他線程不安全的方法,就可能會導致數據的不一致或者錯誤。因此,在使用并行流時,要盡量避免產生副作用,或者使用線程安全的數據結構和方法,或者使用同步機制來保證線程安全。 - 并行流可能會改變結果的順序或者內容:并行流會將數據分成多個塊,然后由不同的線程處理,這可能會導致結果的順序和數據源的順序不一致,或者結果的內容和數據源的內容不一致。例如,如果使用
findAny
操作在并行流中查找任意一個元素,可能會得到不同的結果,因為并行流不保證返回第一個元素。或者,如果使用forEach
操作在并行流中遍歷元素,可能會得到亂序的結果,因為并行流不保證按照數據源的順序執行。因此,在使用并行流時,要注意選擇合適的操作,或者使用sorted
或者forEachOrdered
等操作來保證結果的順序。
// 創建一個 Stream 流對象
Stream<String> animalStream = Stream.of("cat", "dog", "elephant", "fox", "giraffe");// 使用順序流計算所有元素的長度之和
int sum1 = animalStream.mapToInt(String::length).sum();
System.out.println(sum1); // 輸出 23// 使用并行流計算所有元素的長度之和
int sum2 = animalStream.parallel().mapToInt(String::length).sum();
System.out.println(sum2); // 輸出 23,和順序流相同,因為 sum 是一個無狀態的終端操作// 使用順序流查找任意一個以 f 開頭的元素
Optional<String> any1 = animalStream.filter(s -> s.startsWith("f")).findAny();
System.out.println(any1); // 輸出 Optional[fox]// 使用并行流查找任意一個以 f 開頭的元素
Optional<String> any2 = animalStream.parallel().filter(s -> s.startsWith("f")).findAny();
System.out.println(any2); // 輸出 Optional[fox] 或者 Optional[fox, giraffe],因為并行流不保證返回第一個元素// 使用順序流遍歷所有元素
animalStream.forEach(System.out::println);
// 輸出 cat, dog, elephant, fox, giraffe,和數據源的順序相同// 使用并行流遍歷所有元素
animalStream.parallel().forEach(System.out::println);
// 可能輸出 fox, dog, cat, elephant, giraffe,和數據源的順序不同,因為并行流不保證按照數據源的順序執行
6.3 惰性求值
Stream API 中間操作是惰性求值的,也就是說,它們不會立即執行,而是等到遇到一個終端操作時,才會觸發整個操作管道的執行。這樣可以提高性能,避免不必要的計算。例如:
// 創建一個 Stream 流對象
Stream<Integer> numberStream = Stream.of(1, 2, 3, 4, 5);// 使用 filter 和 map 中間操作,但不會立即執行
Stream<Integer> transformedStream = numberStream.filter(n -> n % 2 == 0).map(n -> n * n);// 使用 forEach 終端操作,觸發中間操作的執行
transformedStream.forEach(System.out::println); // 輸出 4, 16
6.4 無狀態和有狀態
Stream API 中間操作可以分為無狀態和有狀態兩種,無狀態的操作是指不依賴于任何狀態,只和當前元素有關的操作,例如 filter
, map
, peek
等;有狀態的操作是指依賴于某些狀態,需要考慮整個 Stream 流的元素的操作,例如 distinct
, sorted
, limit
, skip
等。無狀態的操作通常比有狀態的操作更高效,因為它們不需要維護額外的狀態,也不需要等待所有的元素都處理完畢。有狀態的操作可能會影響并行性能,因為它們需要同步狀態,或者等待所有的元素都到達。例如:
// 創建一個 Stream 流對象
Stream<String> animalStream = Stream.of("cat", "dog", "elephant", "fox", "giraffe");// 使用 filter 和 map 無狀態操作,可以并行執行
Stream<String> upperStream = animalStream.parallel().filter(s -> s.length() == 3).map(String::toUpperCase);// 使用 distinct 和 sorted 有狀態操作,需要同步狀態或者等待所有元素
Stream<String> uniqueStream = animalStream.parallel().distinct().sorted();
6.5 短路和非短路
Stream API 中間操作可以分為短路和非短路兩種,短路的操作是指不需要處理所有的元素,只要滿足某些條件就可以停止的操作,例如 limit
, takeWhile
, dropWhile
等;非短路的操作是指需要處理所有的元素,不能提前停止的操作,例如 filter
, map
, sorted
等。短路的操作通常比非短路的操作更高效,因為它們可以減少不必要的計算,也可以和一些短路的終端操作配合,例如 anyMatch
, findFirst
, findAny
等。非短路的操作可能會影響性能,因為它們需要處理所有的元素,也不能和一些短路的終端操作配合。例如:
// 創建一個 Stream 流對象
Stream<Integer> numberStream = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);// 使用 limit 和 takeWhile 短路操作,可以提前停止
Stream<Integer> takenStream = numberStream.limit(5).takeWhile(n -> n < 4);// 使用 filter 和 map 非短路操作,需要處理所有元素
Stream<Integer> filteredStream = numberStream.filter(n -> n % 2 == 0).map(n -> n * n);