1. Chuyển đổi List → Set và ngược lại
Hãy cùng nhắc lại vì sao ta cần chuyển đổi bộ sưu tập. Trong các bài toán thực tế, ta thường cần:
- Lấy các phần tử duy nhất từ danh sách (ví dụ, danh sách e-mail → tập các địa chỉ duy nhất).
- Xây dựng ánh xạ (Map), chẳng hạn từ danh sách tên tạo map “tên → độ dài tên”.
- Ghép các phần tử thành một chuỗi (ví dụ, để in ra đẹp hơn).
Trước đây, để làm được vậy ta phải viết rất nhiều vòng lặp, điều kiện và collection tạm. Với Stream API, mọi thứ trở nên đơn giản và… “đẹp” hơn!
Ví dụ: lấy tập tên duy nhất từ danh sách
Giả sử ta có một danh sách tên (biết đâu ai đó tự nhập hai lần — chuyện thường!):
List<String> names = List.of("Anna", "Sergey", "Anna", "Mariya", "Ivan", "Sergey");
Mục tiêu — nhận được một collection mà mỗi tên chỉ xuất hiện một lần, tức là tập hợp (Set). Với Stream API điều này chỉ trong một dòng:
Set<String> uniqueNames = names.stream()
.collect(Collectors.toSet());
System.out.println(uniqueNames);
Kết quả:
[Mariya, Ivan, Anna, Sergey]
(Thứ tự trong Set không được đảm bảo — đừng ngạc nhiên nếu bạn thấy thứ tự khác.)
Còn nếu cần chuyển ngược: Set → List?
Đôi khi ta cần chuyển ngược lại — từ tập hợp thành danh sách (ví dụ để sắp xếp hoặc truy cập theo chỉ số):
List<String> namesList = uniqueNames.stream()
.collect(Collectors.toList());
System.out.println(namesList);
2. Chuyển đổi sang Map: Collectors.toMap()
Ví dụ: từ danh sách tên tạo Map “tên → độ dài tên”
Đôi lúc ta muốn làm “nhà bản đồ” thực thụ — xây map! Hãy thử nhé:
List<String> names = List.of("Anna", "Sergey", "Mariya", "Ivan");
Map<String, Integer> nameToLength = names.stream()
.collect(Collectors.toMap(
name -> name, // khóa — chính là tên
name -> name.length() // giá trị — độ dài tên
));
System.out.println(nameToLength);
Kết quả:
{Mariya=6, Ivan=4, Anna=4, Sergey=6}
Điểm quan trọng: khóa trùng lặp
Nếu trong danh sách gốc có các tên giống nhau, thì khi cố gắng thu thập chúng vào Map sẽ phát sinh lỗi IllegalStateException: Duplicate key. Java không thích khi bạn cố đặt hai giá trị cho cùng một khóa.
Xử lý trùng như thế nào?
Bạn có thể chỉ định phải làm gì khi khóa trùng — ví dụ, giữ giá trị đầu tiên hoặc giá trị cuối cùng:
List<String> names = List.of("Anna", "Sergey", "Anna", "Mariya", "Ivan", "Sergey");
Map<String, Integer> nameToLength = names.stream()
.collect(Collectors.toMap(
name -> name,
name -> name.length(),
(oldValue, newValue) -> oldValue // giữ giá trị đầu tiên
));
System.out.println(nameToLength);
Giờ chương trình sẽ không bị lỗi, và trong Map chỉ lấy lần xuất hiện đầu tiên của mỗi tên.
Ví dụ: Map với đối tượng
Hãy phức tạp hơn chút: ta có danh sách người dùng và muốn xây Map “tên → người dùng”:
class User {
String name;
int age;
User(String name, int age) {
this.name = name;
this.age = age;
}
public String toString() {
return name + " (" + age + ")";
}
}
// Ví dụ danh sách người dùng
List<User> users = List.of(
new User("Anna", 25),
new User("Sergey", 30),
new User("Mariya", 22)
);
Map<String, User> nameToUser = users.stream()
.collect(Collectors.toMap(
user -> user.name,
user -> user
));
System.out.println(nameToUser);
Kết quả:
{Mariya=Mariya (22), Anna=Anna (25), Sergey=Sergey (30)}
3. Ghép thành chuỗi: Collectors.joining()
Đôi khi ta không muốn thu thập thành collection mà muốn tạo một chuỗi đẹp để hiển thị cho người dùng hoặc ghi log. Ví dụ, ghép tất cả tên bằng dấu phẩy:
List<String> names = List.of("Anna", "Sergey", "Mariya", "Ivan");
String result = names.stream()
.collect(Collectors.joining(", "));
System.out.println(result);
Kết quả:
Anna, Sergey, Mariya, Ivan
Có thể thêm prefix và suffix
String result = names.stream()
.collect(Collectors.joining(", ", "Danh sách: [", "]"));
System.out.println(result);
Kết quả:
Danh sách: [Anna, Sergey, Mariya, Ivan]
4. Các phép toán kết thúc: forEach, collect, count, anyMatch, allMatch, noneMatch
Phương thức forEach
Ta đã khá quen với forEach: phép toán này thực hiện một hành động cho mỗi phần tử trong stream.
names.stream().forEach(name -> System.out.println("Xin chào, " + name + "!"));
Phương thức collect
Thu thập các phần tử thành collection, chuỗi hoặc cấu trúc khác. Phép toán phổ biến nhất — thu thập vào List hoặc Set bằng Collectors.toList() và Collectors.toSet().
Phương thức count
Đếm số phần tử trong stream.
long count = names.stream()
.filter(name -> name.length() > 4)
.count();
System.out.println("Số tên dài hơn 4 ký tự: " + count);
Các phương thức anyMatch, allMatch, noneMatch
Kiểm tra điều kiện đúng với ít nhất một phần tử (anyMatch), với tất cả (allMatch) hoặc không đúng với bất kỳ phần tử nào (noneMatch).
boolean hasShortName = names.stream()
.anyMatch(name -> name.length() < 4);
System.out.println("Có tên ngắn không? " + hasShortName);
boolean allLong = names.stream()
.allMatch(name -> name.length() > 3);
System.out.println("Tất cả tên dài hơn 3 ký tự? " + allLong);
boolean noneIvan = names.stream()
.noneMatch(name -> name.equals("Ivan"));
System.out.println("Không có Ivan? " + noneIvan);
Kết quả:
Có tên ngắn không? false
Tất cả tên dài hơn 3 ký tự? true
Không có Ivan? false
5. Phép toán kết thúc và trung gian: củng cố khái niệm
Phép toán trung gian (filter, map, distinct, sorted, limit, skip, peek) — trả về Stream mới, có thể xâu chuỗi.
Phép toán kết thúc (forEach, collect, count, anyMatch, allMatch, noneMatch, reduce, findFirst, findAny) — kết thúc stream, sau đó sẽ không còn kết quả nào nữa!
Ví dụ chuỗi thao tác:
List<String> result = users.stream()
.filter(user -> user.age > 20)
.map(user -> user.name.toUpperCase())
.distinct()
.sorted()
.collect(Collectors.toList());
System.out.println(result);
Kết quả:
[ANNA, IVAN, MARIYA, SERGEY]
6. Lỗi thường gặp khi chuyển đổi collection với Stream
Lỗi 1: Không xử lý khóa trùng trong toMap
Nếu trong collection nguồn có khóa trùng mà bạn dùng Collectors.toMap() mà không chỉ rõ hàm gộp, chương trình sẽ ném ngoại lệ. Trong các trường hợp như vậy, luôn chỉ định hàm hợp nhất:
// Giữ giá trị cuối cùng
.toMap(keyMapper, valueMapper, (oldVal, newVal) -> newVal)
Lỗi 2: Dùng forEach thay vì collect
Đôi khi người mới thử “thu thập” collection bằng forEach, ví dụ:
List<String> list = new ArrayList<>();
names.stream().forEach(name -> list.add(name)); // Chạy được, nhưng không theo phong cách Stream!
Tốt hơn hãy dùng collect(Collectors.toList()) — an toàn và gọn gàng hơn.
Lỗi 3: Cố gắng tái sử dụng stream
Stream chỉ dùng được một lần. Sau phép toán kết thúc (ví dụ, collect, forEach) nếu tiếp tục dùng lại cùng Stream sẽ dẫn tới IllegalStateException.
Lỗi 4: Vi phạm nguyên tắc “không tác dụng phụ”
Các phép toán trung gian nên “thuần” (không thay đổi biến bên ngoài). Không nên thay đổi thứ gì ngoài stream bên trong map hoặc filter.
Lỗi 5: Không tính đến thứ tự trong Set và Map
Nếu thứ tự quan trọng, hãy dùng collection phù hợp — ví dụ, LinkedHashSet, TreeMap — và chỉ định collector phù hợp.
GO TO FULL VERSION