Kể từ phiên bản Java 8, Stream API đã trở thành một công cụ mạnh mẽ giúp các nhà phát triển xử lý dữ liệu một cách khai báo (declarative) thay vì mệnh lệnh (imperative). Stream cho phép chúng ta thực hiện các thao tác phức tạp trên tập hợp dữ liệu với cú pháp ngắn gọn, dễ đọc và tối ưu hơn.
Sự khác biệt giữa cách làm truyền thống và Stream API
Hãy xem xét bài toán lọc ra các sản phẩm có giá trị lớn hơn 500 từ một danh sách.
1. Cách tiếp cận truyền thống (Imperative)
List<Integer> prices = Arrays.asList(200, 600, 150, 800, 450);
List<Integer> expensivePrices = new ArrayList<>();
for (Integer price : prices) {
if (price > 500) {
expensivePrices.add(price);
}
}
System.out.println(expensivePrices);
2. Sử dụng Stream API (Declarative)
List<Integer> prices = Arrays.asList(200, 600, 150, 800, 450);
List<Integer> expensivePrices = prices.stream()
.filter(p -> p > 500)
.collect(Collectors.toList());
System.out.println(expensivePrices);
Stream API giúp mã nguồn tập trung vào việc cần làm thay vì cách thức thực hiện việc lặp qua dữ liệu.
Cơ chế và đặc điểm của Stream
- Không lưu trữ: Stream không phải là một cấu trúc dữ liệu, nó chỉ truyền tải dữ liệu từ nguồn (Collection, Array, I/O channel).
- Không thay đổi nguồn: Các thao tác trên Stream trả về kết quả mới nhưng không làm biến đổi dữ liệu gốc.
- Lười biếng (Lazy evaluation): Các thao tác trung gian chỉ được thực thi khi có một thao tác kết thúc được gọi.
- Sử dụng một lần: Một Stream sau khi đã thực thi thao tác kết thúc sẽ không thể tái sử dụng.
Cách khởi tạo Stream
Stream có thể được tạo ra từ nhiều nguồn khác nhau:
// Từ List
List<String> names = Arrays.asList("Java", "Kotlin", "Swift");
Stream<String> nameStream = names.stream();
// Từ Array
String[] techArray = {"Spring", "Hibernate", "Quarkus"};
Stream<String> techStream = Arrays.stream(techArray);
// Từ Map (thông qua EntrySet)
Map<Integer, String> userMap = new HashMap<>();
userMap.put(1, "Admin");
userMap.stream().entrySet().forEach(System.out::println);
Các thao tác phổ biến trong Stream API
Các phương thức trong Stream được chia thành hai nhóm chính: Thao tác trung gian (Intermediate) và Thao tác kết thúc (Terminal).
Thao tác trung gian (Trả về một Stream mới)
| Phương thức | Mô tả |
|---|---|
filter(Predicate) |
Loại bỏ các phần tử không thỏa mãn điều kiện. |
map(Function) |
Ánh xạ hoặc chuyển đổi các phần tử sang một giá trị mới. |
sorted() |
Sắp xếp các phần tử theo thứ tự tự nhiên hoặc theo Comparator. |
distinct() |
Loại bỏ các phần tử trùng lặp. |
limit(n) |
Lấy ra n phần tử đầu tiên. |
skip(n) |
Bỏ qua n phần tử đầu tiên. |
Thao tác kết thúc (Đóng Stream và trả về kết quả)
forEach(): Duyệt qua từng phần tử.count(): Đếm số lượng phần tử trong Stream.collect(): Chuyển đổi Stream về dạng Collection (List, Set, Map).reduce(): Kết hợp các phần tử để tạo ra một kết quả duy nhất (tổng, tích...).
Ví dụ tổng hợp
Giả sử chúng ta có danh sách tên các lập trình viên, cần lọc những tên có độ dài lớn hơn 3 ký tự, chuyển thành chữ hoa, sắp xếp ngược bảng chữ cái và lấy ra 2 người đầu tiên.
List<String> devs = Arrays.asList("An", "Bình", "Cường", "Dũng", "Em");
List<String> processedDevs = devs.stream()
.filter(name -> name.length() > 3)
.map(String::toUpperCase)
.sorted(Comparator.reverseOrder())
.limit(2)
.collect(Collectors.toList());
System.out.println(processedDevs);
// Output: [DŨNG, CƯỜNG]
Những lưu ý khi sử dụng Stream
- Hiệu năng: Mặc dù Stream viết code nhanh hơn, nhưng với các tập dữ liệu cực nhỏ, vòng lặp
fortruyền thống có thể có hiệu năng tốt hơn một chút do không tốn chi phí khởi tạo đối tượng Stream. - Thứ tự thực hiện: Các thao tác như
filternên được đặt trướcsortedhoặcmapđể giảm thiểu số lượng phần tử cần xử lý ở các bước sau. - Parallel Streams: Với dữ liệu cực lớn, có thể sử dụng
parallelStream()để tận dụng đa nhân CPU, nhưng cần cẩn trọng với các vấn đề về thread-safe.