CodeGym /Các khóa học /JAVA 25 SELF /Các collector nâng cao

Các collector nâng cao

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

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/MapcollectingAndThen để “đó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.

1
Khảo sát/đố vui
, cấp độ , bài học
Không có sẵn
Nhóm và tổng hợp
Stream API: nhóm và tổng hợp
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION