Menu

Stream trong Java: giải thích filter, map, collect và reduce

Cách xử lý collection bằng Stream API của Java: filter, map, sorted, collect, count và reduce, xây dựng các pipeline dễ đọc thay vì vòng lặp viết tay.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Pipeline thay cho vòng lặp

Stream là một pipeline để xử lý một dãy giá trị. Bạn bắt đầu từ một nguồn, thường là một collection, nối các thao tác như "chỉ giữ những phần tử này", "biến đổi từng phần tử", "sắp xếp chúng", rồi kết thúc bằng việc gom kết quả lại. Thay vì viết một vòng lặp với một list tạm và một câu lệnh if bên trong, bạn mô tả điều mình muốn và để stream tự duyệt.

Khi đã có lambda, stream trở nên dễ hiểu: mọi thao tác đều nhận một lambda (hoặc method reference) mô tả việc cần làm với một phần tử.

Hãy đọc từ trên xuống: lấy các tên, giữ những tên dài, viết hoa chúng, gom vào một list. List names ban đầu không hề bị sửa: stream tạo ra một kết quả mới và để nguyên nguồn.

Cấu tạo của một stream

Mỗi pipeline có ba phần:

  • Một nguồn: list.stream(), Arrays.stream(array) hoặc Stream.of(a, b, c).
  • Không hoặc nhiều thao tác trung gian: filter, map, sorted, distinct, limit. Mỗi thao tác trả về một stream khác, nên bạn có thể nối chúng lại.
  • Đúng một thao tác cuối: collect, count, forEach, reduce, findFirst. Thao tác này khởi động công việc và tạo ra một kết quả (hoặc một tác dụng phụ).

Việc phân chia này quan trọng vì một quy tắc lúc đầu khiến ai cũng bất ngờ.

Thao tác trung gian là lười

Thao tác trung gian tự nó không làm gì cả. Chúng chỉ ghi lại những gì sẽ xảy ra. Pipeline chỉ chạy khi một thao tác cuối yêu cầu kết quả.

Chạy thử: lambda của map không bao giờ được thực thi. Thêm một thao tác cuối và cả chuỗi bừng tỉnh:

Tính lười này không phải điểm kỳ quặc cần chống lại: nó cho phép stream gộp các thao tác và bỏ qua việc không cần thiết. Nhưng nó có nghĩa là một pipeline không có thao tác cuối thì chẳng làm gì, và đây là một bug "sao chẳng có gì xảy ra?" rất phổ biến.

filter và map: giữ lại và biến đổi

Hai thao tác này gánh phần lớn công việc. filter nhận một Predicate (một lambda trả về boolean) và giữ lại các phần tử thỏa mãn. map nhận một Function và thay mỗi phần tử bằng kết quả khi áp dụng hàm đó.

Lưu ý rằng map có thể đổi kiểu phần tử: ở đây một stream String trở thành một stream Integer. String::length là một method reference, cách viết tắt của lambda w -> w.length().

Thao tác cuối tạo ra kết quả

Khi đã định hình xong stream, một thao tác cuối biến nó thành thứ cụ thể:

Các thao tác cuối thường gặp: collect (gom vào list/set/map), count, forEach, anyMatch / allMatch / noneMatch, findFirst, min / max và reduce. Sau khi một thao tác cuối chạy, stream đã bị tiêu thụ: bạn không thể dùng lại nó. Gọi lại list.stream() để có một pipeline mới.

Gom kết quả

collect cùng với Collectors là công cụ chủ lực để tạo kết quả. Phổ biến nhất là Collectors.toList():

Collectors còn cung cấp toSet(), joining(...), groupingBy(...) và counting(). Từ Java 16 trở đi, bạn có thể thay collect(Collectors.toList()) bằng .toList() ngắn hơn (nó trả về một list không sửa được):

List<String> result = names.stream().map(String::toUpperCase).toList();

sorted, distinct và limit

Các thao tác trung gian này định hình lại stream trước khi bạn gom kết quả:

sorted() không đối số dùng thứ tự tự nhiên; truyền vào một Comparator (ở đây là một lambda) để có thứ tự tùy chỉnh. Comparator.reverseOrder() là cách gọn hơn để sắp xếp giảm dần.

reduce: gộp stream thành một giá trị

Khi bạn cần kết hợp mọi phần tử thành một kết quả duy nhất, như tổng, tích, chuỗi dài nhất, reduce là công cụ tổng quát. Bạn đưa cho nó một giá trị khởi đầu và một hàm gộp hai giá trị:

Với tổng và trung bình đơn giản, các stream chuyên biệt rõ ràng hơn: nums.stream().mapToInt(Integer::intValue).sum(). Hãy dùng reduce khi không có sẵn bộ tổng hợp nào phù hợp.

Stream không thay thế mọi vòng lặp

Stream phát huy sức mạnh khi bạn biến đổi một collection thành một kết quả. Chúng không tự động nhanh hơn vòng lặp, và khá vụng về khi bạn cần thay đổi trạng thái bên ngoài hoặc thoát sớm theo những cách rắc rối. Một quy tắc hay: nếu pipeline đọc lên như một câu rõ ràng, hãy dùng stream; nếu bạn đang cần một biến đếm dùng chung hay một index, một vòng lặp for thông thường là thẳng thắn và hoàn toàn ổn.

Cũng nhớ rằng stream chỉ dùng được một lần. Thao tác trên nó hai lần sẽ ném ra ngoại lệ:

Stream<String> s = names.stream();
s.forEach(System.out::println);
s.count();   // IllegalStateException: stream has already been operated upon

Tiếp theo: Optional

Một số thao tác cuối của stream, như findFirst, min, max, reduce không có giá trị khởi đầu, có thể không tìm thấy gì, nên chúng không trả về một giá trị trần. Chúng trả về một Optional, kiểu chứa của Java cho "có thể có giá trị, có thể không có gì", cuối cùng cho bạn một lựa chọn gọn gàng thay cho việc trả về null. Đó là nội dung của trang tiếp theo.

Câu hỏi thường gặp

Stream trong Java là gì?

Stream là một pipeline để xử lý một dãy phần tử, thường lấy từ một collection, qua chuỗi thao tác như filter, map và sorted, rồi kết thúc bằng một thao tác cuối như collect hay count. Nó không lưu dữ liệu và không sửa nguồn; nó mô tả một phép tính. Bạn tạo stream bằng list.stream() và đọc chuỗi thao tác từ trên xuống như một công thức nấu ăn.

Chuyển stream trở lại thành list trong Java như thế nào?

Kết thúc pipeline bằng một collector cuối: list.stream().filter(...).collect(Collectors.toList()). Từ Java 16+ bạn có thể dùng .toList() ngắn hơn, trả về một list không sửa được. Nếu không có thao tác cuối, sẽ không có gì chạy cả, vì các thao tác trung gian như filter và map là lười (lazy).

Khi nào nên dùng stream thay cho vòng lặp for?

Hãy dùng stream khi bạn biến đổi hoặc lọc một collection thành một kết quả: pipeline đọc lên như một câu rõ ràng ("lấy các tên, giữ tên dài, viết hoa chúng, gom vào một list"). Hãy giữ vòng lặp for thông thường khi bạn cần thay đổi trạng thái bên ngoài, thoát sớm theo cách phức tạp, hoặc logic đơn giản hơn khi viết thành các bước tuần tự. Stream là vì sự rõ ràng, không phải vì tốc độ.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu