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(判斷匹配,終端操作)

操作目的

案例代碼

關鍵説明

過濾符合條件的對象

List<Student> collect = students.stream().filter(s -> s.age > 20).collect(Collectors.toList());

1. filter 是中間操作,需搭配 collect 終端操作才會執行;2. 篩選邏輯通過 Lambda 定義(這裏是 “年齡> 20”)。

判斷是否存在符合條件元素

boolean hasUnderage = students.stream().anyMatch(s -> s.age < 18);

1. anyMatch 是終端操作,直接返回 boolean;2. 短路操作:找到第一個符合條件的元素就停止,效率高。

二、元素轉換:改變元素的 “形式” 或 “內容”

核心需求:提取對象屬性、修改屬性內容(將流中元素轉換為新形式,生成新流)關鍵方法map(元素轉換,中間操作)

操作目的

案例代碼

關鍵説明

提取對象的某個屬性

List<String> collect3 = students.stream().map(Student::getMajor).distinct().collect(Collectors.toList());

1. map(Student::getMajor) 將 Student 對象轉為 String 類型的專業;2. 轉換後流的元素類型從 Student 變為 String

修改屬性內容

List<String> collect1 = students.stream().map(s -> s.getName().toUpperCase()).collect(Collectors.toList());

1. 這裏將 “學生姓名” 轉換為大寫;2. 轉換邏輯可自定義(如拼接字符串、數值計算等)。

三、排序:對列表元素按規則 “排順序”

核心需求:按對象屬性升序 / 降序排列關鍵方法sorted(排序,中間操作)+ Comparator(比較器)

操作目的

案例代碼

關鍵説明

按屬性倒序排序

List<Student> collect2 = students.stream().sorted(Comparator.comparingInt(Student::getAge).reversed()).collect(Collectors.toList());

1. Comparator.comparingInt(Student::getAge) 定義 “按年齡正序”;2. reversed() 反轉順序,實現倒序;3. 排序後仍保留完整 Student 對象。

四、統計計算:對數值類屬性做 “聚合計算”

核心需求:求和、求平均、計數(針對數值型數據,得到單一結果)關鍵方法mapToInt(轉為數值流,中間操作)、sum/average(終端操作)、reduce(通用聚合,終端操作)

操作目的

案例代碼

關鍵説明

求數值總和(簡單場景)

int sum = students.stream().mapToInt(Student::getAge).sum();

1. mapToInt 轉為 IntStream,避免裝箱 / 拆箱,性能高;2. sum() 是 IntStream 專用終端操作,直接返回總和。

求數值總和(自定義場景)

int totalAge = students.stream().map(Student::getAge).reduce(0, (a, b) -> a + b);

1. reduce 是通用聚合方法,適合複雜邏輯(如帶條件的累加);2. 第一個參數是初始值(求和初始為 0),第二個是累加器(a是當前和,b是下一個元素)。

求平均值

OptionalDouble average = students.stream().mapToInt(Student::getAge).average();

1. 返回 OptionalDouble,避免流為空時返回 null;2. 可通過 average.getAsDouble() 獲取具體值(需確保流非空)。

基礎計數(列表大小)

int size = students.size();

1. 直接調用 List 的 size() 方法,適合無需 Stream 其他操作的簡單計數;2. 若需先過濾再計數,可用 students.stream().filter(...).count()

五、去重:消除 “重複” 元素(按屬性 / 對象)

核心需求:按屬性去重(保留屬性列表)、按屬性去重(保留原始對象)關鍵方法distinct(簡單去重,中間操作)、Collectors.toMap(按屬性去重,終端操作)

操作目的

案例代碼

關鍵説明

按屬性去重(得到屬性列表)

List<String> collect3 = students.stream().map(Student::getMajor).distinct().collect(Collectors.toList());

1. 先通過 map 提取專業屬性,再用 distinct 去重;2. 最終得到 “無重複的專業列表”(List<String>)。

按屬性去重(保留原始對象)

List<Student> uniqueByMajor = students.stream().collect(Collectors.toMap(Student::getMajor, s->s, (e,n)->e)).values().stream().collect(Collectors.toList());

1. 利用 Map 的 key 唯一特性:key 是專業,value 是 Student 對象;2. 第三個參數 (e,n)->e 表示 “重複時保留舊對象”;3. 最後通過 values() 提取去重後的 Student 對象。

六、分組與聚合:按規則 “歸類” 元素,或合併元素

核心需求:按屬性分組、合併元素(如拼接字符串)關鍵方法Collectors.groupingBy(分組,終端操作)、Collectors.joining(拼接,終端操作)、reduce(合併,終端操作)

操作目的

案例代碼

關鍵説明

按屬性分組

Map<String, List<Student>> collect5 = students.stream().collect(Collectors.groupingBy(Student::getMajor));

1. 按 “專業” 分組,key 是專業,value 是該專業的所有 Student 對象;2. 適合 “分類統計” 場景(如 “每個專業有多少學生”)。

合併元素為字符串

String collect4 = students.stream().map(Student::getName).collect(Collectors.joining(","));

1. 先提取姓名,再用 joining(",") 按 “逗號” 拼接;2. 無需手動處理循環,直接得到合併後的字符串。

合併元素(通用場景)

String reduce1 = students.stream().map(Student::getMajor).reduce("", (a, b) -> a + b);

1. reduce 也可用於字符串拼接(初始值為空串,累加器是字符串拼接);2. 相比 joiningreduce 更靈活,但 joining 對字符串場景更簡潔。

總結:Stream 操作的核心邏輯(3 步)

  1. 明確需求:先想清楚要做什麼(過濾?轉換?統計?);
  2. 選中間操作:用 filter/map/sorted 等定義 “處理規則”(生成新流,不執行);
  3. 選終端操作:用 collect/sum/anyMatch 等觸發 “執行並得到結果”(終端操作只能有一個)。

Stream 的核心特性(避免誤用)、常見坑點(避坑指南)、Optional 配合使用(安全處理空值)

一、Stream 核心特性:必須記住的 “規則”

這些特性決定了 Stream 的使用邊界,不注意就會出問題:

1. 不可重複消費(Stream 是 “一次性” 的)
  • 問題:一個 Stream 對象只能執行一次終端操作(如 collectsumanyMatch),執行後流會被 “消耗”,再次調用終端操作會報錯。
  • 示例
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 對象,後續 mapfilter 操作調用對象方法時,會直接拋出 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 類型(如 OptionalDoubleOptional<Integer>),其核心作用是 避免空指針,安全處理 “流為空時無結果” 的情況。

錯誤用法:直接 get(),可能拋異常
// 錯誤:若 students 為空,average() 返回 OptionalDouble.empty,getAsDouble() 會拋 NoSuchElementException
double avg = students.stream()
    .mapToInt(Student::getAge)
    .average()
    .getAsDouble();
正確用法:用 orElseifPresent 等方法處理空值

方法

作用

示例代碼

orElse(默認值)

無結果時返回默認值

double avg = students.stream().mapToInt(Student::getAge).average().orElse(0.0);

orElseGet(供應器)

無結果時通過函數生成默認值(更靈活)

double avg = students.stream().mapToInt(Student::getAge).average().orElseGet(() -> 0.0);

ifPresent(消費者)

有結果時才執行操作(避免判斷 null)

students.stream().mapToInt(Student::getAge).average().ifPresent(avg -> System.out.println("平均年齡:" + avg));

四、補充小技巧:提升開發效率

1. 鏈式操作的 “最優順序”:先過濾,再轉換 / 排序
  • 原理filter 會減少流中元素的數量,後續的 mapsorted 等操作處理的數據量減少,效率更高。
  • 示例
// 推薦:先過濾(年齡>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());