Stream 操作按 “核心需求場景” 分類歸納,每個類別下整合 “操作目的、核心方法、案例代碼、關鍵説明”
四個基本語法概念:
Stream:的操作是鏈式執行的,每個操作都會基於上一步的結果生成新的流。
map:將流中的每個元素按照指定的規則(函數)進行轉換,生成一個包含轉換後元素的新流。
collect() 方法是一個終端操作, “執行收集動作” 的入口,而具體 “收集成什麼樣子”,由傳入的 Collector(收集器)決定。
另一篇文章的補充(例子更清晰一些):
// 創建一個學生列表
List students = Arrays.asList(
new Student("Alice", 22, "Computer Science"),
new Student("Bob", 21, "Mathematics"),
new Student("Charlie", 23, "Chemistry"),
new Student("David", 19, "Biology"),
new Student("Eva", 20, "Mathematics"),
new Student("Frank", 22, "History"),
new Student("Grace", 21, "Biology")
);
一、基礎篩選:從列表中 “挑出” 符合條件的元素
核心需求:過濾元素、判斷元素是否存在(不改變元素本身,只篩選保留 / 判斷)關鍵方法:filter(過濾元素,中間操作)、anyMatch/allMatch/noneMatch(判斷匹配,終端操作)
|
操作目的
|
案例代碼
|
關鍵説明
|
|
過濾符合條件的對象
|
|
1. |
|
判斷是否存在符合條件元素
|
|
1. |
二、元素轉換:改變元素的 “形式” 或 “內容”
核心需求:提取對象屬性、修改屬性內容(將流中元素轉換為新形式,生成新流)關鍵方法:map(元素轉換,中間操作)
|
操作目的
|
案例代碼
|
關鍵説明
|
|
提取對象的某個屬性
|
|
1. |
|
修改屬性內容
|
|
1. 這裏將 “學生姓名” 轉換為大寫;2. 轉換邏輯可自定義(如拼接字符串、數值計算等)。
|
三、排序:對列表元素按規則 “排順序”
核心需求:按對象屬性升序 / 降序排列關鍵方法:sorted(排序,中間操作)+ Comparator(比較器)
|
操作目的
|
案例代碼
|
關鍵説明
|
|
按屬性倒序排序
|
|
1. |
四、統計計算:對數值類屬性做 “聚合計算”
核心需求:求和、求平均、計數(針對數值型數據,得到單一結果)關鍵方法:mapToInt(轉為數值流,中間操作)、sum/average(終端操作)、reduce(通用聚合,終端操作)
|
操作目的
|
案例代碼
|
關鍵説明
|
|
求數值總和(簡單場景)
|
|
1. |
|
求數值總和(自定義場景)
|
|
1. |
|
求平均值
|
|
1. 返回 |
|
基礎計數(列表大小)
|
|
1. 直接調用 |
五、去重:消除 “重複” 元素(按屬性 / 對象)
核心需求:按屬性去重(保留屬性列表)、按屬性去重(保留原始對象)關鍵方法:distinct(簡單去重,中間操作)、Collectors.toMap(按屬性去重,終端操作)
|
操作目的
|
案例代碼
|
關鍵説明
|
|
按屬性去重(得到屬性列表)
|
|
1. 先通過 |
|
按屬性去重(保留原始對象)
|
|
1. 利用 |
六、分組與聚合:按規則 “歸類” 元素,或合併元素
核心需求:按屬性分組、合併元素(如拼接字符串)關鍵方法:Collectors.groupingBy(分組,終端操作)、Collectors.joining(拼接,終端操作)、reduce(合併,終端操作)
|
操作目的
|
案例代碼
|
關鍵説明
|
|
按屬性分組
|
|
1. 按 “專業” 分組, |
|
合併元素為字符串
|
|
1. 先提取姓名,再用 |
|
合併元素(通用場景)
|
|
1. |
總結:Stream 操作的核心邏輯(3 步)
- 明確需求:先想清楚要做什麼(過濾?轉換?統計?);
- 選中間操作:用
filter/map/sorted等定義 “處理規則”(生成新流,不執行); - 選終端操作:用
collect/sum/anyMatch等觸發 “執行並得到結果”(終端操作只能有一個)。
Stream 的核心特性(避免誤用)、常見坑點(避坑指南)、Optional 配合使用(安全處理空值)
一、Stream 核心特性:必須記住的 “規則”
這些特性決定了 Stream 的使用邊界,不注意就會出問題:
1. 不可重複消費(Stream 是 “一次性” 的)
- 問題:一個 Stream 對象只能執行一次終端操作(如
collect、sum、anyMatch),執行後流會被 “消耗”,再次調用終端操作會報錯。 - 示例:
Stream stream = students.stream().filter(s -> s.age > 20);
stream.collect(Collectors.toList()); // 第一次終端操作:正常
stream.count(); // 第二次終端操作:報錯(IllegalStateException: stream has already been operated upon or closed)
- 解決:每次需要處理時,重新生成流(如
students.stream()每次調用都會創建新流)。
2. 並行流(Parallel Stream):高效處理大數據,但需注意線程安全
- 適用場景:數據量極大(如上萬條數據),想利用多核 CPU 加速處理時,可將普通流轉為並行流。
- 用法:在
stream()後加parallel(),或直接用parallelStream():
// 並行流計算年齡總和(數據量大時比普通流快)
int totalAge = students.parallelStream()
.mapToInt(Student::getAge)
.sum();
- 坑點:並行流會多線程處理元素,若操作中涉及 線程不安全的集合(如
ArrayList),會導致數據錯誤:
// 錯誤示例:ArrayList 線程不安全,並行流添加元素會丟數據
List names = new ArrayList<>();
students.parallelStream()
.map(Student::getName)
.forEach(names::add); // 可能出現元素丟失或數組越界
// 正確示例:用線程安全的集合,或直接 collect
List names = students.parallelStream()
.map(Student::getName)
.collect(Collectors.toList()); // collect 內部會處理線程安全
二、常見坑點:避開這些 “隱形錯誤”
1. 空指針問題(流中存在 null 元素)
- 問題:若流中包含
null對象,後續map、filter操作調用對象方法時,會直接拋出NullPointerException。 - 示例:
// 假設 students 中混入一個 null(如從數據庫查詢時可能出現)
List students = Arrays.asList(new Student("Alice",22,"CS"), null, new Student("Bob",21,"Math"));
// 錯誤:調用 null.getName() 會拋空指針
students.stream().map(Student::getName).collect(Collectors.toList());
// 解決:先過濾掉 null 元素
students.stream()
.filter(Objects::nonNull) // 過濾 null,Objects::nonNull 是方法引用,等價於 s -> s != null
.map(Student::getName)
.collect(Collectors.toList());
2. sorted 排序的 “隱性要求”
- 問題:若對自定義對象(如
Student)用sorted()且未指定比較器,會拋出ClassCastException—— 因為Student未實現Comparable接口。 - 示例:
// 錯誤:Student 未實現 Comparable,直接 sorted() 會報錯
students.stream().sorted().collect(Collectors.toList());
// 正確:必須指定比較器(如按年齡排序)
students.stream()
.sorted(Comparator.comparingInt(Student::getAge))
.collect(Collectors.toList());
3. Collectors.toMap 的 “鍵重複” 問題
- 問題:用
Collectors.toMap時,若流中存在 重複的 key(如兩個學生專業相同),且未指定 “重複鍵處理規則”,會拋出IllegalStateException。 - 示例:
// 錯誤:若有兩個學生專業都是 "Mathematics",會拋鍵重複異常
Map map = students.stream()
.collect(Collectors.toMap(Student::getMajor, s -> s)); // 無重複處理規則
// 正確:指定重複鍵時保留舊值或新值
Map map = students.stream()
.collect(Collectors.toMap(
Student::getMajor,
s -> s,
(oldVal, newVal) -> oldVal // 重複時保留舊值;若想保留新值,改寫成 (o,n) -> n
));,>,>
三、Optional 配合使用:安全處理 “無結果” 場景
之前的 average()、reduce() 會返回 Optional 類型(如 OptionalDouble、Optional<Integer>),其核心作用是 避免空指針,安全處理 “流為空時無結果” 的情況。
錯誤用法:直接 get(),可能拋異常
// 錯誤:若 students 為空,average() 返回 OptionalDouble.empty,getAsDouble() 會拋 NoSuchElementException
double avg = students.stream()
.mapToInt(Student::getAge)
.average()
.getAsDouble();
正確用法:用 orElse、ifPresent 等方法處理空值
|
方法
|
作用
|
示例代碼
|
|
|
無結果時返回默認值
|
|
|
|
無結果時通過函數生成默認值(更靈活)
|
|
|
|
有結果時才執行操作(避免判斷 null)
|
|
四、補充小技巧:提升開發效率
1. 鏈式操作的 “最優順序”:先過濾,再轉換 / 排序
- 原理:
filter會減少流中元素的數量,後續的map、sorted等操作處理的數據量減少,效率更高。 - 示例:
// 推薦:先過濾(年齡>20),再提取姓名(減少 map 處理的元素)
List names = students.stream()
.filter(s -> s.age > 20)
.map(Student::getName)
.collect(Collectors.toList());
// 不推薦:先 map 再 filter,多處理了年齡≤20的元素
List names = students.stream()
.map(Student::getName)
.filter(name -> students.stream().filter(s -> s.getName().equals(name)).findFirst().get().age > 20)
.collect(Collectors.toList());
2. 用 peek 調試 Stream 流程
- 作用:
peek是中間操作,可在流的每個元素處理時 “插入調試邏輯”(如打印元素),不改變元素本身,方便定位問題。 - 示例:
// 查看過濾、轉換的中間結果
students.stream()
.peek(s -> System.out.println("原始元素:" + s.getName())) // 打印原始元素
.filter(s -> s.age > 20)
.peek(s -> System.out.println("過濾後元素:" + s.getName())) // 打印過濾後元素
.map(Student::getName)
.peek(name -> System.out.println("轉換後元素:" + name)) // 打印轉換後元素
.collect(Collectors.toList());