CodeGym /Các khóa học /JAVA 25 SELF /Phương thức reduce và collect: tổng hợp dữ liệu

Phương thức reduce và collect: tổng hợp dữ liệu

JAVA 25 SELF
Mức độ , Bài học
Có sẵn

1. Phương thức reduce: phép thu gọn đa dụng

Trong lập trình, ta thường cần “thu gọn” một tập hợp về một giá trị cuối cùng: tính tổng, tìm tích, nối chuỗi, tính một chỉ số tổng hợp hoặc gom các phần tử vào cấu trúc mới. Trước đây điều này được làm thủ công bằng vòng lặp và biến tích lũy. Ngày nay Stream API cung cấp cách tiếp cận trang nhã — các phương thức đa dụng reducecollect, cho phép viết mã gọn gàng và khai báo hơn.

  • reduce — thu gọn luồng về một giá trị cuối cùng (tổng, tích, nối chuỗi, v.v.).
  • collect — chuyển luồng thành một collection, chuỗi, map hoặc cấu trúc tùy ý.

Hãy đi từng bước.

Tại sao cần reduce?

reduce là phương thức kết thúc (terminal) “thu gọn” các phần tử của luồng về một giá trị, sử dụng một hàm tích lũy. Có thể hình dung như duyệt qua collection và cộng dồn kết quả theo từng bước.

Chữ ký của phương thức reduce

Trong Stream API có ba biến thể chính của 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 — hàm nhận giá trị tích lũy hiện tại và phần tử tiếp theo, trả về kết quả mới.
  • identity — giá trị khởi tạo của bộ tích lũy (ví dụ, 0 cho tổng, 1 cho tích).
  • combiner — dùng trong các luồng song song để hợp nhất kết quả trung gian.

Ví dụ sử dụng reduce

Ví dụ 1: Tính tổng các số

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

// reduce không có identity — kết quả là Optional
Optional<Integer> sum1 = numbers.stream()
    .reduce((a, b) -> a + b);

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

// reduce có identity — luôn có kết quả
int sum2 = numbers.stream()
    .reduce(0, (a, b) -> a + b);

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

Ví dụ 2: Tích của tất cả các số

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

Ví dụ 3: Nối chuỗi

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

Ví dụ 4: Tìm phần tử lớn nhất

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

Ví dụ 5: Tổng độ dài của tất cả chuỗi

List<String> texts = List.of("mèo", "chó", "voi");
int totalLength = texts.stream()
    .map(String::length)
    .reduce(0, Integer::sum);
System.out.println(totalLength); // 9

Reduce hoạt động như thế nào

Logic của reduce tương đương với vòng lặp sau:

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

Nếu không truyền identity, giá trị khởi tạo sẽ là phần tử đầu tiên của luồng, và phương thức trả về Optional (có thể rỗng nếu luồng rỗng).

2. Phương thức collect: chuyển đổi đa năng

collect là phương thức kết thúc, biến luồng thành collection, chuỗi, map hoặc bất kỳ cấu trúc nào khác. Để làm điều đó ta dùng “collector” (Collector) mô tả quá trình lắp ráp. Thường thì ta dùng sẵn từ lớp Collectors.

Các collector phổ biến nhất

  • Collectors.toList() — gom phần tử vào List.
  • Collectors.toSet() — gom phần tử vào Set.
  • Collectors.toMap() — gom phần tử vào Map.
  • Collectors.joining() — ghép các chuỗi thành một.
  • Collectors.groupingBy() — phân nhóm phần tử theo tiêu chí.
  • Collectors.counting() — đếm số lượng phần tử.
  • Collectors.summarizingInt() — thu thập thống kê cho số nguyên (tổng, trung bình, min/max).

Ví dụ sử dụng collect

Ví dụ 1: Gom vào danh sách

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]

Ví dụ 2: Gom vào tập (Set)

Set<String> nameSet = names.stream()
    .collect(Collectors.toSet());
System.out.println(nameSet); // [Anya, Boris, Vasya] (thứ tự không được đảm bảo)

Ví dụ 3: Gom thành chuỗi

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

Ví dụ 4: Gom vào Map

Giả sử ta có lớp:

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; }
}

Tạo map “tên → phòng ban”:

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 // xử lý trùng tên
    ));

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

Ví dụ 5: Gom các phần tử duy nhất vào Set

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

Ví dụ 6: Thu thập thống kê cho số

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. So sánh: khi nào dùng reduce, khi nào — collect

reduce — khi cần một giá trị cuối cùng bằng một phép toán nhị phân: tổng, tích, max, nối chuỗi.

collect — khi cần gom phần tử vào collection/map/chuỗi hoặc thực hiện các phép tổng hợp phức tạp với Collector. Với các bài toán này, collect thường mạnh và hiệu quả hơn.

Bảng: reduce vs collect

Nhiệm vụ Dùng cái gì Ví dụ
Tổng các số
reduce
reduce(0, Integer::sum)
Tích
reduce
reduce(1, (a, b) -> a * b)
Gom vào List
collect
collect(Collectors.toList())
Gom vào Map
collect
collect(Collectors.toMap(...))
Phân nhóm
collect
collect(Collectors.groupingBy(...))
Nối chuỗi reduce / collect reduce("", String::concat) hoặc Collectors.joining()

4. Bài tập thực hành

Bài 1: Tính tổng độ dài của tất cả chuỗi trong danh sách

List<String> words = List.of("mèo", "chó", "voi");

int totalLength = words.stream()
    .mapToInt(String::length)
    .sum(); // hoặc dùng reduce: .reduce(0, Integer::sum)

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

Bài 2: Gom các phần tử duy nhất vào Set

List<String> fruits = List.of("táo", "lê", "táo", "cam");

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

System.out.println(uniqueFruits); // [táo, lê, cam]

Bài 3: Xây Map từ danh sách đối tượng

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 // nếu trùng tên
    ));

System.out.println(nameToDept);

Bài 4: Nối tất cả tên bằng dấu phẩy

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

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

5. Lưu ý triển khai và sắc thái

Optional và reduce

Nếu dùng reduce mà không có identity, kết quả là Optional. Điều này an toàn: nếu luồng rỗng, kết quả cũng rỗng. Đừng quên xử lý đúng: ifPresent(...), orElse(...), orElseThrow(...).

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

Collector tự viết: nếu bạn muốn “mạo hiểm”

Bạn có thể tự viết Collector nếu các collector chuẩn chưa đủ. Nhưng cho 99% bài toán — các collector dựng sẵn từ Collectors là đủ.

Collector và stream song song

Các collector dựng sẵn từ Collectors được thiết kế để hoạt động đúng với parallelStream(). Đừng tự thêm phần tử vào một collection dùng chung có thể thay đổi trong forEach trên luồng song song — bạn sẽ gặp race condition.

6. Những lỗi thường gặp khi làm việc với reduce và collect

Lỗi số 1: Không kiểm tra Optional sau reduce. Nếu luồng rỗng, reduce không có identity trả về Optional rỗng. Gọi get() sẽ ném NoSuchElementException. Hãy dùng ifPresent, orElse hoặc orElseThrow.

Lỗi số 2: Cố gom collection bằng reduce. Có thể làm vậy, nhưng collect được thiết kế tốt hơn và nhanh hơn cho mục đích này:

// Không hiệu quả!
List<String> list = stream.reduce(
    new ArrayList<>(),
    (acc, elem) -> { acc.add(elem); return acc; },
    (acc1, acc2) -> { acc1.addAll(acc2); return acc1; }
);
// Tốt hơn là:
List<String> list2 = stream.collect(Collectors.toList());

Lỗi số 3: Không xử lý khóa trùng trong toMap. Nếu khóa trùng, sẽ có ngoại lệ. Hãy thêm đối số thứ ba cho toMap để giải quyết xung đột.

Lỗi số 4: Dùng collection có thể thay đổi trong stream song song mà không đồng bộ. Trong collect hãy dùng các collector chuẩn — chúng hoạt động đúng ở chế độ song song. Đừng gọi list.add() trong forEach trên parallelStream().

Lỗi số 5: Nhầm lẫn giữa reduce và collect cho bài toán phức tạp. reduce — cho các tổng hợp đơn giản (tổng, max). collect — để gom vào collection, phân nhóm, xây Map và tổng hợp phức tạp.

Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION