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 reduce và collect, 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ố | |
|
| Tích | |
|
| Gom vào List | |
|
| Gom vào Map | |
|
| Phân nhóm | |
|
| 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.
GO TO FULL VERSION