CodeGym /Các khóa học /JAVA 25 SELF /Phân tích các lỗi điển hình khi làm việc với Stream

Phân tích các lỗi điển hình khi làm việc với Stream

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

1. Lỗi: sử dụng lại stream

Stream (Stream) trong Java là công cụ dùng một lần. Ngay khi bạn thực hiện một thao tác kết thúc (ví dụ, collect(), forEach(), count()), stream sẽ bị đóng và không còn dùng được nữa.

Ví dụ lỗi

Stream<String> stream = Stream.of("a", "b", "c");
stream.forEach(System.out::println); // OK
stream.forEach(System.out::println); // Ném IllegalStateException!

Kết quả:

a
b
c
Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed

Cách tránh

Nếu cần thực hiện nhiều thao tác — hãy tạo stream mới mỗi lần:

List<String> list = List.of("a", "b", "c");
list.stream().forEach(System.out::println); // OK
list.stream().count(); // OK

Mẹo: Stream giống như cốc dùng một lần ở căng tin: dùng xong thì bỏ, lần sau lấy cái mới cho đồ uống tiếp theo.

2. Lỗi: thay đổi collection trong lúc stream

Nếu bạn sửa đổi collection nguồn trong khi duyệt qua stream, bạn sẽ gặp ConcurrentModificationException. Đây là cơn đau thường gặp khi lọc và xóa phần tử.

Ví dụ lỗi

List<String> names = new ArrayList<>(List.of("Alice", "Bob", "Charlie"));
names.stream().forEach(name -> {
    if (name.startsWith("A")) {
        names.remove(name); // BÙM!
    }
});

Kết quả:

Exception in thread "main" java.util.ConcurrentModificationException

Cách đúng

Hãy dùng các phương thức không sửa đổi collection trong khi duyệt, hoặc thu kết quả vào một collection mới:

names.removeIf(name -> name.startsWith("A")); // OK, removeIf an toàn

// Hoặc dùng Stream API:
List<String> filtered = names.stream()
    .filter(name -> !name.startsWith("A"))
    .collect(Collectors.toList());

Ghi nhớ: Stream thích sự “sạch sẽ” — đừng đụng vào collection gốc khi stream đang “ăn” nó.

3. Lỗi: quên limit với stream vô hạn

Các phương thức Stream.iterateStream.generate có thể tạo ra stream vô hạn. Nếu quên giới hạn kích thước bằng limit, chương trình có thể treo mãi (hoặc cho đến khi OutOfMemoryError).

Ví dụ lỗi

Stream<Integer> infinite = Stream.iterate(0, n -> n + 1);
infinite.forEach(System.out::println); // Chương trình sẽ không bao giờ kết thúc!

Cách đúng

Giới hạn stream bằng limit:

Stream<Integer> finite = Stream.iterate(0, n -> n + 1).limit(10);
finite.forEach(System.out::println); // Sẽ in ra các số từ 0 đến 9

Gợi ý: Stream vô hạn giống như một bộ phim dài tập không có tập cuối: phải có ai đó dừng lại, và tốt nhất là bạn!

4. Lỗi: xử lý null không đúng

Stream API không ưa null. Nếu trong collection có null, nhiều thao tác có thể ném NullPointerException, đặc biệt khi bạn gọi phương thức trên các phần tử.

Ví dụ lỗi

List<String> names = Arrays.asList("Alice", null, "Bob");
names.stream()
    .map(String::toUpperCase) // BÙM! Tại null
    .forEach(System.out::println);

Kết quả:

Exception in thread "main" java.lang.NullPointerException

Cách đúng

Lọc bỏ các giá trị null từ sớm:

names.stream()
    .filter(Objects::nonNull)
    .map(String::toUpperCase)
    .forEach(System.out::println);

Làm việc với Optional

Nếu bạn có Stream<Optional<T>>, hãy dùng flatMap(Optional::stream) (Java 9+) để “trải phẳng”:

List<Optional<String>> optionals = List.of(Optional.of("A"), Optional.empty(), Optional.of("B"));
optionals.stream()
    .flatMap(Optional::stream)
    .forEach(System.out::println); // Chỉ in "A" và "B"

Gợi ý: Cố gắng đừng thêm null vào collection. Nếu không tránh được, hãy lọc chúng ngay từ đầu.

5. Lỗi: mất thứ tự phần tử

Một số thao tác (ví dụ, flatMap, concat) có thể ảnh hưởng đến thứ tự phần tử, đặc biệt khi bạn làm việc với stream song song hoặc hợp nhất các collection khác nhau.

Ví dụ lỗi

List<Integer> list1 = List.of(1, 2, 3);
List<Integer> list2 = List.of(4, 5, 6);
Stream<Integer> combined = Stream.concat(list1.stream(), list2.stream());
combined.forEach(System.out::print); // 123456 — thứ tự được giữ nguyên

// Nhưng nếu dùng stream song song:
Stream<Integer> par = Stream.concat(list1.parallelStream(), list2.parallelStream());
par.forEach(System.out::print); // Thứ tự có thể bất ngờ!

Cách đúng

Nếu thứ tự quan trọng — hãy tránh stream song song hoặc dùng các phương thức đảm bảo thứ tự (forEachOrdered):

par.forEachOrdered(System.out::print);

Thực tế: Stream API mặc định giữ thứ tự, trừ khi chính bạn đã “làm vỡ” nó.

6. Lỗi: hợp nhất collection không hiệu quả

Với các thao tác trên tập hợp, tốt hơn nên dùng Set thay vì List để tránh trùng lặp và tăng hiệu năng. Việc viết vòng lặp lồng nhau cũng không hiệu quả ở nơi có thể dùng flatMap.

Ví dụ lỗi

List<String> list1 = List.of("a", "b", "c");
List<String> list2 = List.of("b", "c", "d");

// Không hiệu quả:
List<String> union = new ArrayList<>(list1);
for (String s : list2) {
    if (!union.contains(s)) { // contains trên List — O(n)
        union.add(s);
    }
}

// Hiệu quả với Set và Stream API:
Set<String> unionSet = Stream.concat(list1.stream(), list2.stream())
    .collect(Collectors.toSet());

Hoặc với phép hiệu:

// Không hiệu quả:
List<String> diff = new ArrayList<>(list1);
diff.removeAll(list2); // OK, nhưng nếu làm qua stream và list.contains — sẽ chậm

// Hiệu quả:
Set<String> set2 = new HashSet<>(list2);
List<String> difference = list1.stream()
    .filter(s -> !set2.contains(s))
    .collect(Collectors.toList());

Gợi ý: Với phần tử duy nhất, luôn dùng Set. Với collection lớn — tránh vòng lặp lồng nhau, ưu tiên stream + flatMap.

7. Lỗi: dùng lại các toán tử kết thúc

Đôi khi muốn “chia” stream thành hai nhánh, ví dụ vừa thu phần tử vào danh sách vừa đếm số lượng. Nhưng stream “dùng một lần”, thao tác thứ hai sẽ gây lỗi.

Ví dụ lỗi

Stream<String> stream = Stream.of("a", "b", "c");
List<String> list = stream.collect(Collectors.toList());
long count = stream.count(); // BÙM! IllegalStateException

Cách đúng

Nếu cần dùng stream nhiều lần — hãy thu vào collection trước:

List<String> list = Stream.of("a", "b", "c").collect(Collectors.toList());
long count = list.size();

Gợi ý: Nếu muốn “vừa ăn cá vừa ngồi lên stream” — hãy thu vào collection trước.

8. Lỗi: quên close() đối với tài nguyên

Nếu bạn dùng stream gắn với tài nguyên (ví dụ, Files.lines(Path)), đừng quên đóng chúng sau khi sử dụng để tránh rò rỉ.

Ví dụ lỗi

Stream<String> lines = Files.lines(Path.of("file.txt"));
lines.forEach(System.out::println); // OK, nhưng tài nguyên chưa được đóng!

Cách đúng

Dùng try-with-resources:

try (Stream<String> lines = Files.lines(Path.of("file.txt"))) {
    lines.forEach(System.out::println);
}

Gợi ý: Stream với tệp giống như ấm nước: đun xong thì tắt!

9. Lỗi: xử lý Optional và null không đúng trong flatMap

Khi làm việc với collection kiểu List<Optional<T>>, nhiều người dùng map(Optional::get), điều này gây ngoại lệ nếu bên trong Optional rỗng.

Ví dụ lỗi

List<Optional<String>> optionals = List.of(Optional.of("A"), Optional.empty());
optionals.stream()
    .map(Optional::get) // BÙM! NoSuchElementException
    .forEach(System.out::println);

Cách đúng

Hãy dùng flatMap(Optional::stream) (Java 9+):

optionals.stream()
    .flatMap(Optional::stream)
    .forEach(System.out::println); // Chỉ "A"

Gợi ý: Optional không chỉ là một lớp bao bọc, mà là công cụ để làm việc an toàn với tình huống không có giá trị.

10. Lỗi: triển khai equals/hashCode không đúng

Các thao tác trên tập hợp (Set, distinct, hợp, giao) phụ thuộc vào việc triển khai đúng các phương thức equalshashCode.

Ví dụ lỗi

class Student {
    String name;
    int age;
    // Không có equals và hashCode!
}

Set<Student> students = new HashSet<>();
students.add(new Student("Alice", 20));
students.add(new Student("Alice", 20));
System.out.println(students.size()); // 2, dù lẽ ra phải coi là trùng!

Cách đúng

Hãy ghi đè equalshashCode cho các lớp của bạn nếu định dùng trong Set, Map hoặc cho thao tác distinct.

11. Lỗi: dùng stream sau collect

Sau thao tác kết thúc (ví dụ, collect) stream sẽ đóng. Bất kỳ cố gắng tiếp tục với chính stream đó sẽ dẫn đến lỗi.

Ví dụ lỗi

Stream<String> stream = Stream.of("a", "b", "c");
List<String> list = stream.collect(Collectors.toList());
stream.filter(s -> s.startsWith("a")).forEach(System.out::println); // Exception!

Cách đúng

Tạo stream mới từ collection:

list.stream().filter(s -> s.startsWith("a")).forEach(System.out::println);

12. Lỗi: sử dụng parallelStream không đúng

Nhiều người nghĩ rằng parallelStream() luôn tăng tốc xử lý. Thực tế, với collection nhỏ hoặc thao tác đơn giản, song song hóa có thể còn làm chậm do chi phí quản lý luồng.

Ví dụ lỗi

List<Integer> numbers = IntStream.range(0, 10).boxed().collect(Collectors.toList());
numbers.parallelStream().forEach(System.out::println); // Có thể chậm hơn stream() thông thường

Cách đúng

Chỉ dùng parallelStream cho các collection thực sự lớn hoặc tính toán nặng, nhất là khi thao tác thuần (không có tác dụng phụ) và có tính kết hợp.

13. Lỗi: quên toán tử kết thúc

Các thao tác trung gian (map, filter, flatMap v.v.) sẽ không chạy cho đến khi gọi một toán tử kết thúc (collect, forEach, count v.v.).

Ví dụ lỗi

Stream<String> stream = Stream.of("a", "b", "c").map(String::toUpperCase);
// Sẽ không có gì xảy ra!

Cách đúng

Nhất định phải gọi toán tử kết thúc:

stream.forEach(System.out::println);
1
Khảo sát/đố vui
, cấp độ , bài học
Không có sẵn
Hợp nhất và phép chiếu
Stream API: hợp nhất và phép chiếu
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION