1. Giới thiệu
Khi bạn làm việc với collection thông qua Stream API, thường không chỉ cần nhóm các phần tử mà còn muốn làm ngay điều gì đó với chúng: chuyển đổi, lọc, thu thập vào kiểu collection khác. Để làm việc này trong Java có downstream collector — các collector lồng bên trong, được áp dụng cho từng nhóm hoặc phần dữ liệu.
Downstream collector là gì?
Đó là collector được áp dụng cho kết quả của việc grouping hoặc partitioning. Ví dụ, bạn có thể nhóm sinh viên theo khóa học với groupingBy, và bên trong mỗi nhóm chỉ thu thập tên hoặc chỉ những sinh viên xuất sắc (ví dụ, với ngưỡng 4.5 theo GPA).
Ví dụ
mapping
Cho phép chuyển đổi phần tử của nhóm trước khi thu thập.
Map<Integer, List<String>> namesByCourse = students.stream()
.collect(Collectors.groupingBy(
Student::getCourse,
Collectors.mapping(Student::getName, Collectors.toList())
));
- Nhóm sinh viên theo khóa học.
- Với mỗi nhóm chỉ thu thập tên (không phải toàn bộ đối tượng).
filtering
Cho phép lọc các phần tử bên trong nhóm (ví dụ, giữ lại GPA >= 4.5).
Map<Integer, List<Student>> honorsByCourse = students.stream()
.collect(Collectors.groupingBy(
Student::getCourse,
Collectors.filtering(s -> s.getGpa() >= 4.5, Collectors.toList())
));
Trong mỗi nhóm chỉ giữ lại sinh viên xuất sắc.
flatMapping
Cho phép “trải phẳng” các collection lồng nhau bên trong nhóm.
Map<String, Set<String>> tagsByAuthor = books.stream()
.collect(Collectors.groupingBy(
Book::getAuthor,
Collectors.flatMapping(
book -> book.getTags().stream(),
Collectors.toSet()
)
));
Với mỗi tác giả, thu thập các tag duy nhất của tất cả sách của họ.
partitioningBy với downstream
Hoạt động tương tự groupingBy, nhưng chia thành hai nhóm theo điều kiện boolean.
Map<Boolean, List<String>> namesByPassed = students.stream()
.collect(Collectors.partitioningBy(
s -> s.getGpa() >= 3.0,
Collectors.mapping(Student::getName, Collectors.toList())
));
Chia sinh viên thành đỗ và trượt; bên trong mỗi nhóm — chỉ có tên.
2. teeing: tổng hợp đồng thời bằng hai collector
Đôi khi cần tính đồng thời nhiều số liệu tổng hợp trên stream: ví dụ vừa tổng vừa trung bình, hoặc tối thiểu và tối đa. Để làm điều này, từ Java 12+ có collector teeing.
teeing hoạt động như thế nào?
Bạn truyền vào hai collector và một hàm hợp nhất kết quả của chúng.
Cú pháp:
Collectors.teeing(collector1, collector2, (result1, result2) -> ...)
Ví dụ
Min + max
Optional<MinMax> minMax = numbers.stream()
.collect(Collectors.teeing(
Collectors.minBy(Integer::compareTo),
Collectors.maxBy(Integer::compareTo),
(min, max) -> min.isPresent() && max.isPresent() ? new MinMax(min.get(), max.get()) : null
));
Tìm min và max cùng lúc.
Tổng + trung bình
Result result = numbers.stream()
.collect(Collectors.teeing(
Collectors.summingInt(Integer::intValue),
Collectors.averagingInt(Integer::intValue),
(sum, avg) -> new Result(sum, avg)
));
Nhận về một đối tượng chứa tổng và giá trị trung bình.
Ví dụ: báo cáo lương
SalaryStats stats = employees.stream()
.collect(Collectors.teeing(
Collectors.summingInt(Employee::getSalary),
Collectors.averagingInt(Employee::getSalary),
SalaryStats::new
));
Trong SalaryStats lưu cả tổng lẫn trung bình.
3. toUnmodifiableList/Set/Map và collectingAndThen để “đóng băng”
Trong các phiên bản Java hiện đại có những collection không thể thay đổi sau khi tạo — bất biến (unmodifiable). Điều này tiện lợi cho API, nơi quan trọng là kết quả không bị thay đổi nhầm.
toUnmodifiableList/Set/Map
- Trả về collection bất biến.
- Mọi cố gắng thêm/xóa phần tử sẽ ném UnsupportedOperationException.
Ví dụ:
List<String> names = students.stream()
.map(Student::getName)
.collect(Collectors.toUnmodifiableList());
Map<Integer, Student> byId = students.stream()
.collect(Collectors.toUnmodifiableMap(Student::getId, Function.identity()));
collectingAndThen
Cho phép áp dụng một hàm lên kết quả của collector — ví dụ, “đóng băng” collection.
List<String> names = students.stream()
.map(Student::getName)
.collect(Collectors.collectingAndThen(
Collectors.toList(),
Collections::unmodifiableList
));
Đầu tiên thu vào một danh sách thường, sau đó biến nó thành bất biến.
Ví dụ với Set:
Set<String> tags = books.stream()
.flatMap(book -> book.getTags().stream())
.collect(Collectors.collectingAndThen(
Collectors.toSet(),
Set::copyOf // Java 10+
));
4. Các trường hợp pipeline
Báo cáo và thống kê theo lát cắt
Với các collector nâng cao, bạn có thể xây dựng các báo cáo và thống kê phức tạp “chỉ trong một dòng”.
Ví dụ: lương trung bình theo phòng ban
Map<String, Double> avgSalaryByDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.averagingInt(Employee::getSalary)
));
Ví dụ: top 3 sản phẩm đắt nhất theo danh mục
Map<String, List<Product>> top3ByCategory = products.stream()
.collect(Collectors.groupingBy(
Product::getCategory,
Collectors.collectingAndThen(
Collectors.toList(),
list -> list.stream()
.sorted(Comparator.comparing(Product::getPrice).reversed())
.limit(3)
.toList()
)
));
Kết quả bất biến như một hợp đồng API
Nếu phương thức của bạn trả về một collection không thể thay đổi, điều đó giúp tránh lỗi vô tình và làm API đáng tin cậy hơn.
public List<String> getTags() {
return tags.stream()
.collect(Collectors.toUnmodifiableList());
}
Người dùng sẽ không thể gọi getTags().add("thẻ mới") — sẽ có ngoại lệ.
Ví dụ: báo cáo với nhiều số liệu tổng hợp (teeing)
public SalaryReport getSalaryReport(List<Employee> employees) {
return employees.stream()
.collect(Collectors.teeing(
Collectors.averagingInt(Employee::getSalary),
Collectors.summingInt(Employee::getSalary),
SalaryReport::new
));
}
Trong SalaryReport lưu cả lương trung bình và tổng lương.
5. Lỗi thường gặp và lưu ý
Lỗi số 1: Quên tính bất biến. Nếu bạn trả về một danh sách thường, nó có thể bị thay đổi. Hãy dùng toUnmodifiableList/Set/Map hoặc collectingAndThen để “đóng băng” kết quả.
Lỗi số 2: Chọn sai downstream collector. Nếu cần chuyển đổi phần tử trong nhóm — dùng mapping; nếu cần lọc — filtering; nếu cần “trải phẳng” collection lồng nhau — flatMapping.
Lỗi số 3: UnsupportedOperationException. Xảy ra khi cố gắng thay đổi collection được thu thập qua toUnmodifiableList/Set/Map hoặc đã “đóng băng” qua collectingAndThen.
Lỗi số 4: Mất tính duy nhất/xung đột khóa. toUnmodifiableSet yêu cầu phần tử duy nhất, còn toUnmodifiableMap yêu cầu khóa duy nhất; nếu không, bạn sẽ nhận ngoại lệ trong lúc thu thập.
GO TO FULL VERSION