1. Lỗi với biểu thức lambda: bắt biến
Trong Java, các biểu thức lambda có thể sử dụng biến từ ngữ cảnh bên ngoài. Nhưng có một ràng buộc: các biến như vậy phải là final hoặc "effectively final" (effectively final), tức là không bị thay đổi sau khi khởi tạo.
Ví dụ lỗi
int sum = 0;
List<Integer> list = List.of(1, 2, 3, 4, 5);
list.forEach(n -> sum += n); // Lỗi biên dịch!
Vì sao?
Trình biên dịch báo lỗi: biến được dùng trong lambda thì phải là final hoặc effectively final, nhưng sum lại bị thay đổi bên trong lambda.
Cách tránh?
- Hãy dùng các thao tác kết thúc (terminal) của stream không cần biến bên ngoài: mapToInt + sum().
- Trong trường hợp bất đắc dĩ — dùng các "container" như AtomicInteger hoặc mảng một phần tử (nhưng đây chỉ là mẹo).
int sum = list.stream().mapToInt(Integer::intValue).sum();
Tương tự
Hãy tưởng tượng lambda là "kẻ du hành thời gian": nó "ghi nhớ" giá trị của biến tại thời điểm được tạo và không thể quan sát cách giá trị đó thay đổi. Cố gắng thay đổi sẽ gây ra "nghịch lý ông nội", và trình biên dịch sẽ không cho biên dịch chương trình.
2. Lỗi về phạm vi và this
Trong biểu thức lambda, từ khóa this trỏ tới đối tượng bên ngoài, chứ không phải lớp ẩn danh (khác với khi dùng lớp ẩn danh).
Ví dụ
public class Example {
int value = 42;
void foo() {
Runnable r = () -> {
System.out.println(this.value); // this là Example, không phải Runnable!
};
r.run();
}
}
Lưu ý: khi chuyển mã từ lớp ẩn danh sang lambda, logic của this thay đổi — hãy tính tới điều đó để tránh kết quả bất ngờ.
3. Vấn đề với trạng thái có thể thay đổi (side effects)
Cách tiếp cận hàm khuyến khích không có tác dụng phụ: hàm không thay đổi trạng thái bên ngoài và không làm biến đổi các collection/biến bên ngoài.
List<String> names = new ArrayList<>(List.of("Anna", "Boris", "Vika"));
List<String> newNames = new ArrayList<>();
names.forEach(name -> {
if (name.startsWith("A")) {
newNames.add(name); // Tác dụng phụ!
}
});
Đoạn mã này "chạy được", nhưng ít dự đoán hơn và nguy hiểm khi dùng với parallelStream() (nguy cơ race condition và ngoại lệ). Việc kiểm thử và bảo trì cũng khó hơn.
Đúng: hãy dùng các thao tác tạo ra kết quả mới một cách rõ ràng mà không thay đổi trạng thái bên ngoài.
List<String> newNames = names.stream()
.filter(name -> name.startsWith("A"))
.collect(Collectors.toList());
4. Lỗi về kiểu và generics
Java là ngôn ngữ kiểu tĩnh. Đôi khi trình biên dịch không thể suy luận kiểu từ các lambda hay chuỗi thao tác quá phức tạp.
Ví dụ
List<Object> objects = List.of(1, "string", 3.14);
List<String> strings = objects.stream()
.filter(obj -> obj instanceof String)
.map(obj -> (String) obj)
.collect(Collectors.toList());
Trông có vẻ hợp lý, nhưng bất kỳ lỗi gõ hay ép kiểu sai nào cũng có thể dẫn tới lỗi biên dịch hoặc, tệ hơn, ClassCastException khi chạy.
Cách tránh?
- Thêm kiểu tường minh nếu suy luận kiểu "vấp ngã".
- Đừng ngại viết <String> hoặc tham số hóa lambda: (String s) -> ....
- Kiểm tra khả năng tương thích kiểu khi chuyển đổi.
Trường hợp điển hình với Optional
Optional<String> opt = Optional.of("hello");
opt.map(s -> s.length()); // Kết quả — Optional<Integer>
Nếu bạn kỳ vọng Optional<String> nhưng lại nhận Optional<Integer>, hãy kiểm tra xem hàm của bạn trả về gì.
5. Tác dụng phụ trong lambda và xử lý song song
Các stream song song (parallelStream()) cộng với tác dụng phụ là một kết hợp nguy hiểm.
Ví dụ
List<Integer> numbers = IntStream.range(0, 1000).boxed().collect(Collectors.toList());
List<Integer> results = new ArrayList<>();
numbers.parallelStream().forEach(n -> results.add(n)); // NGUY HIỂM!
Điều gì có thể xảy ra?
- Mất dữ liệu hoặc trùng lặp dữ liệu.
- ConcurrentModificationException hoặc các bug "bí ẩn".
Làm đúng như thế nào?
- Dùng các collection an toàn cho đa luồng: ConcurrentLinkedQueue, CopyOnWriteArrayList.
- Tốt hơn nữa — tránh hoàn toàn tác dụng phụ và gom kết quả qua collect(...).
List<Integer> results = numbers.parallelStream()
.map(n -> n)
.collect(Collectors.toList());
6. Mất tính dễ đọc: "spaghetti stream" và chuỗi thao tác dài
Phong cách hàm rất tốt, cho đến khi chuỗi thao tác biến thành "hóa đơn siêu dài từ siêu thị".
List<String> result = list.stream()
.filter(s -> s.length() > 2)
.map(String::trim)
.map(s -> s.toUpperCase())
.filter(s -> s.contains("JAVA"))
.sorted()
.distinct()
.collect(Collectors.toList());
Gợi ý:
- Chia nhỏ chuỗi thao tác thành các khối logic.
- Đưa các lambda phức tạp ra các phương thức riêng với tên rõ nghĩa.
- Thêm chú thích khi cần — ngay cả trong mã Stream.
7. Tên biến và hàm kém rõ ràng
Tên quá ngắn (x, y, z) gây khó hiểu.
list.stream()
.map(x -> x.trim())
.filter(y -> y.length() > 3)
.map(z -> z.toUpperCase())
.forEach(System.out::println);
Hãy dùng các tên có nghĩa, đặc biệt khi lambda nhiều dòng hoặc thể hiện logic không tầm thường.
8. Lỗi với null và Optional
Stream API và các functional interface không thích null. Truyền null vào lambda hoặc stream là nguyên nhân thường gặp của NullPointerException.
List<String> list = Arrays.asList("a", null, "b");
list.stream()
.map(String::toUpperCase) // Boom! NPE ở phần tử thứ hai
.forEach(System.out::println);
Làm đúng:
- Lọc null từ sớm: .filter(Objects::nonNull).
- Dùng Optional để biểu diễn rõ ràng giá trị vắng mặt.
9. Vấn đề về kiểu trả về trong các hàm kết hợp
Khi dùng compose và andThen rất dễ nhầm lẫn thứ tự áp dụng hàm và kiểu kỳ vọng.
Function<String, Integer> parse = Integer::parseInt;
Function<Integer, Integer> square = x -> x * x;
Function<String, Integer> parseAndSquare = parse.andThen(square);
// Hoạt động: trước parse, sau square
Function<String, Integer> squareThenParse = parse.compose(square);
// Lỗi! square nhận Integer, còn parse cần String
Bài học: luôn kiểm tra thứ tự áp dụng và sự phù hợp của kiểu.
10. Vấn đề với checked exceptions trong lambda
Các functional interface trong gói java.util.function không cho phép ném checked exceptions (ví dụ, IOException). Nếu bên trong lambda cần mã có thể ném chúng, hãy tự xử lý ngoại lệ.
Function<String, String> readFile = path -> {
try {
return Files.readString(Path.of(path));
} catch (IOException e) {
throw new RuntimeException(e); // Hoặc xử lý theo cách khác
}
};
Nếu không, trình biên dịch sẽ không cho phép dùng hàm như vậy trong stream hoặc collection.
GO TO FULL VERSION