1. 引言
大家好!歡迎來到本系列博客的第三篇。在前兩篇文章中,我們已經領略了 Java 8 中 行為參數化 和 Lambda 表達式 的魅力。
- 在第 1 章 Java行為參數化:從啰嗦到簡潔的代碼進化中,我們了解到如何通過將行為(代碼塊)作為參數傳遞給方法,使代碼更靈活、可復用。
- 在第 2 章 Java 8 Lambda表達式詳解:從入門到實踐中,我們深入學習了 Lambda 表達式,它是實現行為參數化的簡潔而強大的工具。
強烈建議先閱讀前兩篇文章,它們為理解今天的主題——Java 8 中的“流”(Streams)——奠定了基礎。
那么,什么是“流”?它為何如此重要?
簡而言之,Java 8 的“流”提供了一種全新的、聲明式的處理數據的方式。它允許你以類似于 SQL 查詢的風格操作集合(及其他數據源),無需編寫冗長的循環和條件語句。
想象一下工廠的流水線:原材料(數據)從一端進入,經過一系列處理工序(操作),最終產出成品。Java 8 中,“流”就像這條流水線,數據在其中流動,我們可以通過各種“流操作”對其進行 篩選
、轉換
、排序
、分組
等。
本篇我們將深入探討“流”的方方面面:
- 流的定義
- 流的特性
- 流與集合的區別
- 流的核心操作
- 如何利用流編寫更簡潔、高效、易于理解的代碼
讓我們一起開啟 Java 8“流”的探索之旅!
2. 流是什么?(What are Streams?)
引言中,我們用流水線類比了“流”。現在,讓我們揭開“流”的神秘面紗。
流是“從支持數據處理操作的源生成的一系列元素”。
——《Java 8 in Action》
讓我們拆解這個定義:
-
一系列元素: 與集合類似,流也是一系列元素的集合。你可以把一堆蘋果放進籃子(集合),也可以把它們放在流水線(流)上。關鍵在于,流關注的是如何處理這些元素,而不是如何存儲它們。
-
源: 流中的元素從哪里來?答案是“源”。它可以是:
- 集合 (List, Set 等)
- 數組
- I/O 資源 (文件等)
- 生成函數 (例如,產生無限序列的函數) 流本身不存儲數據,它只是從源頭獲取數據。
-
數據處理操作: 這是流的核心!流提供了一套豐富的操作,讓你對數據進行各種處理,類似數據庫查詢操作:
filter
: 篩選符合條件的元素。map
: 將元素轉換為另一種形式(如小寫字母轉大寫)。reduce
: 將所有元素組合成一個結果(如求和)。sort
: 排序。- … 還有很多!
-
內部迭代: 通常,我們用
for
循環或forEach
顯式遍歷集合(外部迭代)。而流則不同,它在內部迭代。你只需要告訴流_你想要做什么_,無需關心_如何做_。這使代碼更簡潔,也更容易優化(如并行處理)。
流不是新的數據結構,而是更高層次的抽象。它專注于 做什么(數據處理),而不是 怎么做(迭代細節)。流像管道,數據從源頭流入,經過一系列處理,產生結果。這種聲明式編程風格使代碼更易讀、維護。
3. 流與集合(Streams vs. Collections)
Java 8 的「流」常與集合(Collections)比較。雖都用于處理數據,但兩者差異顯著。理解這些差異對于有效使用流至關重要。
相同點:
- 存儲元素: 流和集合都可存儲一系列元素。
不同點:
特性 | 集合 (Collections) | 流 (Streams) |
---|---|---|
主要目的 | 存儲和訪問元素 | 對元素進行計算 |
何時計算 | 元素在加入集合時就已計算好 | 元素在需要時才計算(延遲計算/惰性求值) |
迭代方式 | 外部迭代(用戶代碼控制迭代) | 內部迭代(流庫自身控制迭代) |
遍歷次數 | 可以多次遍歷 | 只能遍歷一次 |
數據修改 | 可以添加、刪除、修改集合中的元素 | 流操作通常不修改數據源 |
數據結構 | 是一種數據結構,主要目的是以特定的時間/空間復雜度存儲和訪問數據 | 不是數據結構,它沒有存儲空間,主要目的是對數據源進行計算。 |
詳細解釋幾個關鍵區別:
3.1 只能遍歷一次
這是流的重要限制。一旦對流執行終端操作(如 forEach
、collect
),流就被“消費”,不能再用。再次遍歷會拋 IllegalStateException
。
代碼示例:
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
Stream<String> nameStream = names.stream();// 第一次遍歷:打印名字
nameStream.forEach(System.out::println);// 第二次遍歷:會拋出異常!
// nameStream.forEach(System.out::println); // java.lang.IllegalStateException: stream has already been operated upon or closed
這與集合形成對比,集合可多次遍歷。
3.2 外部迭代與內部迭代
- 外部迭代(集合): 編寫顯式循環(如
for-each
)遍歷集合,并處理元素。你完全掌控迭代過程。 - 內部迭代(流): 只需告訴流你想做什么(如篩選長度大于3的名字),流內部進行迭代和處理。無需編寫循環,代碼更簡潔。
代碼示例:
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");// 外部迭代(集合)
List<String> longNames1 = new ArrayList<>();
for (String name : names) {if (name.length() > 3) {longNames1.add(name);}
}
System.out.println(longNames1); // [Alice, Charlie, David]// 內部迭代(流)
List<String> longNames2 = names.stream().filter(name -> name.length() > 3).collect(Collectors.toList());
System.out.println(longNames2); // [Alice, Charlie, David]
流(內部迭代)代碼更簡潔、易讀,更接近聲明式編程。我們描述了想要什么(篩選長度大于3的名字),未指定如何做(循環和條件判斷)。
3.3 延遲計算/惰性求值
這是流的重要特性。流的中間操作(如filter
,map
)延遲計算。遇到終端操作前,中間操作不執行。終端操作觸發時,才計算。
4. 流操作詳解 (Stream Operations in Detail)
流的強大在于其豐富的操作,讓你以聲明式方式處理數據。操作分兩類:中間操作和終端操作。理解這兩類操作及如何協同工作,是掌握流的關鍵。
4.1 中間操作 (Intermediate Operations)
特點:
- 返回另一個流: 每個中間操作返回新流。可將多個中間操作鏈接,形成“流水線”。
- 延遲執行(Lazy): 中間操作不立即執行,只構建流水線。終端操作觸發時,中間操作才執行。
常見中間操作:
操作 | 描述 | 示例 |
---|---|---|
filter | 篩選符合條件的元素 | stream.filter(x -> x > 5) |
map | 將每個元素映射為另一個元素(類型可能不同) | stream.map(String::toUpperCase) |
limit | 截取流的前 N 個元素 | stream.limit(10) |
skip | 跳過流的前 N 個元素 | stream.skip(5) |
distinct | 去除流中的重復元素(根據 equals ) | stream.distinct() |
sorted | 對流中的元素排序(自然排序或根據 Comparator ) | stream.sorted() stream.sorted(Comparator.reverseOrder()) |
peek | 對流中每個元素執行一個操作,但不改變流內容(主要用于調試) | stream.peek(System.out::println) |
flatMap | 將每個元素轉換為一個流,然后將這些流合并為一個流。 | stream.flatMap(Collection::stream) |
代碼示例 (中間操作鏈):
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Eve");List<String> result = names.stream().filter(name -> name.length() > 3) // 篩選長度大于3的名字.map(String::toLowerCase) // 轉小寫.sorted() // 排序.collect(Collectors.toList()); // 收集結果System.out.println(result); // [alice, charlie, david]
filter
、map
、sorted
是中間操作。它們鏈接成流水線。注意,直到 collect
(終端操作)被調用,中間操作才執行。
4.2 終端操作 (Terminal Operations)
特點:
- 產生結果或副作用: 終端操作觸發流水線執行,產生結果(非流值)或副作用(如打印)。
- 消費流: 終端操作執行后,流被消費,不能再用。
常見終端操作:
操作 | 描述 | 示例 |
---|---|---|
forEach | 對流中每個元素執行一個操作(副作用) | stream.forEach(System.out::println) |
count | 返回流中元素個數 | long count = stream.count() |
collect | 將流中元素收集到集合(或其他數據結構) | List<String> list = stream.collect(Collectors.toList()) |
reduce | 將流中元素組合成一個值(如求和、求最大值) | Optional<Integer> sum = stream.reduce(Integer::sum) |
anyMatch | 檢查是否至少有一個元素匹配給定條件 | boolean hasLongName = stream.anyMatch(s -> s.length() > 5) |
allMatch | 檢查是否所有元素都匹配給定條件 | boolean allUpperCase = stream.allMatch(s -> Character.isUpperCase(s.charAt(0))) |
noneMatch | 檢查是否沒有元素匹配給定條件 | boolean noEmptyString = stream.noneMatch(String::isEmpty) |
findFirst | 返回流中第一個元素(Optional) | Optional<String> first = stream.findFirst() |
findAny | 返回流中任意一個元素(Optional,并行流中更常用) | Optional<String> any = stream.findAny() |
代碼示例 (終端操作):
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);// 求和
int sum = numbers.stream().reduce(0, Integer::sum); // 初始值為0,用 Integer.sum() 累加
System.out.println("Sum: " + sum); // Sum: 15// 查找第一個偶數
Optional<Integer> firstEven = numbers.stream().filter(n -> n % 2 == 0).findFirst();
firstEven.ifPresent(System.out::println); // 2 (若存在偶數)// 檢查是否所有數字都大于0
boolean allPositive = numbers.stream().allMatch(n -> n > 0);
System.out.println("All positive: " + allPositive); // All positive: true
5. 流的“按需計算”(On-Demand Computation)
前面多次提到流的“延遲計算”/“惰性求值”。現在深入探討。
5.1 什么是“按需計算”?
流中元素只在真正需要時才計算。與集合對比,集合中所有元素在創建時就已存在于內存。
5.2 為什么“按需計算”重要?
帶來幾個關鍵優勢:
-
效率提升: 若非所有元素都需處理,“按需計算”可避免不必要計算,提高效率。處理大數據集時,優勢明顯。
-
短路操作: “按需計算”使“短路操作”(如
findFirst
、anyMatch
)成為可能。找到滿足條件的元素,就無需處理剩余元素。 -
無限流: “按需計算”使創建“無限流”(Infinite Streams)成為可能。無限流無固定結尾,可根據需要生成無限多元素。
5.3 “按需計算”如何工作?
通過中間操作和終端操作協同實現。
- 中間操作: “懶惰”。只構建處理流水線,不立即執行。
- 終端操作: “急切”。終端操作被調用,觸發流水線執行。
終端操作需要元素時,流水線上中間操作才處理數據源。中間操作通常非一次處理一個元素,而是按需逐個處理。
代碼示例(演示“按需計算”):
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);Optional<Integer> firstEvenGreaterThan5 = numbers.stream().filter(n -> {System.out.println("Filtering: " + n); // 打印過濾操作的中間結果return n % 2 == 0;}).filter(n -> {System.out.println("Filtering again: "+n);return n > 5;}).findFirst();firstEvenGreaterThan5.ifPresent(n -> System.out.println("Result: " + n));
輸出:
Filtering: 1
Filtering: 2
Filtering again: 2
Filtering: 3
Filtering: 4
Filtering again: 4
Filtering: 5
Filtering: 6
Filtering again: 6
Result: 6
分析:
從輸出可見:
- 并非所有數字都被
filter
處理。 findFirst
找到第一個滿足條件的元素(6),后續元素不再處理。- 兩個
filter
非獨立,而是交替執行。
這就是“按需計算”。流只處理必要元素,找到 findFirst
要求的結果。
6.總結
Java 8 的流(Streams)是一種強大而優雅的數據處理工具。它通過聲明式、函數式的風格,使代碼更簡潔、易讀、高效。
在這篇文章中,我們深入探討了:
- 流的本質: 一種支持數據處理操作的元素序列,強調“做什么”而非“怎么做”。
- 流與集合的區別: 延遲計算、內部迭代、一次性遍歷等。
- 流的操作: 中間操作(構建流水線)和終端操作(觸發計算)。
- 按需計算: 流的關鍵特性,提高效率、支持短路操作和無限流。
掌握了流,你就掌握了 Java 8 中最強大的武器之一。在后續的文章中,我們會進一步探索流的高級用法,包括并行流、自定義收集器等。敬請期待!