CodeGym /課程 /JAVA 25 SELF /reduce 與 collect 方法:資料彙總

reduce 與 collect 方法:資料彙總

JAVA 25 SELF
等級 31 , 課堂 1
開放

1. reduce 方法:通用歸約

在程式設計中,常常需要「把集合歸約」成一個最終值:計算總和、求乘積、拼接字串、計算彙總指標,或把元素組成新的結構。以往我們會用迴圈搭配累加變數手動完成。如今 Stream API 提供了更優雅的方式——通用方法 reducecollect,讓你能以更精簡、宣告式的方式撰寫程式。

  • reduce —— 將串流歸約為單一最終值(總和、乘積、連接等)。
  • collect —— 將串流轉換為集合、字串、映射(map)或任意的資料結構。

我們按順序來看。

為什麼需要 reduce?

reduce 是一個終端方法,透過累加器函式把串流元素「歸約」成單一值。可以把它想成對集合做一次走訪,逐步累積結果。

reduce 方法的簽名

Stream API 中,reduce() 有三個主要變體:

Optional<T> reduce(BinaryOperator<T> accumulator)
T reduce(T identity, BinaryOperator<T> accumulator)
<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner)
  • accumulator —— 接收目前的累積值與下一個元素,並回傳新的結果的函式。
  • identity —— 累加器的初始值(例如總和用 0,乘積用 1)。
  • combiner —— 在平行串流中用來合併中間結果。

reduce 的使用範例

範例 1:數字總和

List<Integer> numbers = List.of(1, 2, 3, 4, 5);

// 不帶 identity 的 reduce — 回傳 Optional
Optional<Integer> sum1 = numbers.stream()
    .reduce((a, b) -> a + b);

System.out.println(sum1.orElse(0)); // 15

// 帶有 identity 的 reduce — 一定有結果
int sum2 = numbers.stream()
    .reduce(0, (a, b) -> a + b);

System.out.println(sum2); // 15

範例 2:所有數字的乘積

int product = numbers.stream()
    .reduce(1, (a, b) -> a * b);
System.out.println(product); // 120

範例 3:字串串接

List<String> words = List.of("Java", "Stream", "API");
String phrase = words.stream()
    .reduce("", (a, b) -> a + " " + b);
System.out.println(phrase.trim()); // Java Stream API

範例 4:尋找最大元素

Optional<Integer> max = numbers.stream()
    .reduce(Integer::max);
max.ifPresent(System.out::println); // 5

範例 5:所有字串長度總和

List<String> texts = List.of("kot", "sobaka", "slon");
int totalLength = texts.stream()
    .map(String::length)
    .reduce(0, Integer::sum);
System.out.println(totalLength); // 14

reduce 如何運作

reduce 的邏輯等同於以下迴圈:

T result = identity;
for (T element : collection) {
    result = accumulator.apply(result, element);
}
return result;

如果未提供 identity,起始值會取串流的第一個元素,方法回傳 Optional(當串流為空時即為空)。

2. collect 方法:通用轉換

collect 是一個終端方法,可將串流轉成集合、字串、映射(map)或任何其他結構。這仰賴「收集器」(Collector)來描述收集的過程。實務上我們多半直接使用 Collectors 類別提供的現成收集器。

最常用的收集器

  • Collectors.toList() —— 收集元素成 List
  • Collectors.toSet() —— 收集元素成 Set
  • Collectors.toMap() —— 收集元素成 Map
  • Collectors.joining() —— 將多個字串拼接為一個。
  • Collectors.groupingBy() —— 依條件分組元素。
  • Collectors.counting() —— 計算元素數量。
  • Collectors.summarizingInt() —— 彙整數值統計(總和、平均、最小/最大)。

collect 的使用範例

範例 1:收集成 List

List<String> names = List.of("Anya", "Boris", "Vasya", "Anya");
List<String> uniqueNames = names.stream()
    .distinct()
    .collect(Collectors.toList());
System.out.println(uniqueNames); // [Anya, Boris, Vasya]

範例 2:收集成集合 (Set)

Set<String> nameSet = names.stream()
    .collect(Collectors.toSet());
System.out.println(nameSet); // [Anya, Boris, Vasya](順序不保證)

範例 3:收集成字串

String csv = names.stream()
    .collect(Collectors.joining(", "));
System.out.println(csv); // Anya, Boris, Vasya, Anya

範例 4:收集成 Map

假設我們有一個類別:

public class Employee {
    private String name;
    private String department;
    public Employee(String name, String department) {
        this.name = name;
        this.department = department;
    }
    public String getName() { return name; }
    public String getDepartment() { return department; }
}

建立「姓名 → 部門」的對映:

List<Employee> employees = List.of(
    new Employee("Anya", "IT"),
    new Employee("Boris", "HR"),
    new Employee("Vasya", "IT")
);

Map<String, String> nameToDept = employees.stream()
    .collect(Collectors.toMap(
        Employee::getName,
        Employee::getDepartment,
        (oldValue, newValue) -> newValue // 處理姓名重複
    ));

System.out.println(nameToDept); // {Anya=IT, Boris=HR, Vasya=IT}

範例 5:收集唯一元素到 Set

Set<String> unique = names.stream()
    .collect(Collectors.toSet());
System.out.println(unique);

範例 6:彙總數值統計

List<Integer> numbers = List.of(1, 2, 3, 4, 5);
IntSummaryStatistics stats = numbers.stream()
    .collect(Collectors.summarizingInt(Integer::intValue));
System.out.println(stats.getSum());     // 15
System.out.println(stats.getAverage()); // 3.0
System.out.println(stats.getMax());     // 5
System.out.println(stats.getMin());     // 1

3. 比較:什麼時候用 reduce,什麼時候用 collect

reduce —— 當你需要透過二元運算得到單一最終值時:總和、乘積、最大值、字串連接。

collect —— 當你要把元素收集成集合/映射/字串,或用 Collector 執行更複雜的彙總時。對這類任務,collect 通常更強大也更高效。

表格:reduce 與 collect

任務 該用什麼 範例
數字總和
reduce
reduce(0, Integer::sum)
乘積
reduce
reduce(1, (a, b) -> a * b)
收集成 List
collect
collect(Collectors.toList())
收集成 Map
collect
collect(Collectors.toMap(...))
分組
collect
collect(Collectors.groupingBy(...))
字串串接 reduce / collect reduce("", String::concat)Collectors.joining()

4. 實作練習

練習 1:計算清單中所有字串長度的總和

List<String> words = List.of("kot", "sobaka", "slon");

int totalLength = words.stream()
    .mapToInt(String::length)
    .sum(); // 或使用 reduce:.reduce(0, Integer::sum)

System.out.println(totalLength); // 14

練習 2:收集唯一元素到 Set

List<String> fruits = List.of("yabloko", "grusha", "yabloko", "apelsin");

Set<String> uniqueFruits = fruits.stream()
    .collect(Collectors.toSet());

System.out.println(uniqueFruits); // [yabloko, grusha, apelsin]

練習 3:由物件清單建立 Map

List<Employee> employees = List.of(
    new Employee("Anya", "IT"),
    new Employee("Boris", "HR"),
    new Employee("Vasya", "IT")
);

Map<String, String> nameToDept = employees.stream()
    .collect(Collectors.toMap(
        Employee::getName,
        Employee::getDepartment,
        (oldValue, newValue) -> newValue // 若姓名重複
    ));

System.out.println(nameToDept);

練習 4:用逗號串接所有姓名

String allNames = employees.stream()
    .map(Employee::getName)
    .collect(Collectors.joining(", "));

System.out.println(allNames); // Anya, Boris, Vasya

5. 實作特點與眉角

Optional 與 reduce

若使用不帶 identityreduce,結果為 Optional。這很安全:當串流為空,結果也會是空的。別忘了正確處理它:ifPresent(...)orElse(...)orElseThrow(...)

Optional<Integer> max = numbers.stream().reduce(Integer::max);
max.ifPresent(System.out::println);

自訂收集器:如果你想挑戰進階用法

如果標準收集器不夠用,你可以自行撰寫 Collector。但對 99% 的情況,Collectors 中的現成收集器已經足夠。

收集器與平行串流

Collectors 中的收集器設計上能正確支援 parallelStream()。不要在平行串流中的 forEach 內,手動把元素加入共享且可變的集合——你會遇到資料競爭。

6. 使用 reduce 與 collect 的常見錯誤

錯誤 1:在 reduce 之後沒有檢查 Optional。 如果串流為空,不帶 identityreduce 會回傳空的 Optional。此時呼叫 get() 會拋出 NoSuchElementException。請使用 ifPresentorElseorElseThrow

錯誤 2:試圖用 reduce 來收集集合。 雖然可以這樣做,但對此目的 collect 更合適也更快:

// 效率不佳!
List<String> list = stream.reduce(
    new ArrayList<>(),
    (acc, elem) -> { acc.add(elem); return acc; },
    (acc1, acc2) -> { acc1.addAll(acc2); return acc1; }
);
// 較佳作法:
List<String> list2 = stream.collect(Collectors.toList());

錯誤 3:在 toMap 中沒有處理重複鍵。 如果鍵相同,會丟出例外。請在 toMap 中提供第三個參數以解決衝突。

錯誤 4:在平行串流中未同步就使用可變集合。collect 中請使用標準收集器——它們在平行模式下能正確運作。不要在 parallelStream()forEach 裡做 list.add()

錯誤 5:在複雜任務上混用 reduce 與 collect。 reduce 適合簡單彙總(總和、最大值)。collect 則用於收集到集合、分組、建立 Map 與複雜彙總。

1
任務
JAVA 25 SELF, 等級 31, 課堂 1
上鎖
尋找一週內的最高溫度 🌡️
尋找一週內的最高溫度 🌡️
1
任務
JAVA 25 SELF, 等級 31, 課堂 1
上鎖
創造魔法咒語 📜
創造魔法咒語 📜
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION