1. Biểu thức lambda và collections
Hãy nhớ lại cách xử lý collections trước khi có biểu thức lambda. Giả sử ta có một danh sách chuỗi và muốn in ra màn hình:
List<String> list = Arrays.asList("mèo", "chó", "nhím");
for (String s : list) {
System.out.println(s);
}
Mọi thứ khá đơn giản, nhưng nếu muốn xóa tất cả chuỗi rỗng khỏi danh sách, ta phải viết vòng lặp với điều kiện, đôi khi còn phải dùng iterator (nếu không sẽ có ConcurrentModificationException). Hoặc, chẳng hạn, sắp xếp theo độ dài chuỗi:
Collections.sort(list, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
});
Ngay cả với tác vụ đơn giản như vậy — đã 5 dòng mã và đầy dấu ngoặc “ồn ào”. Tối ưu thế nào? Câu trả lời bạn đã biết: biểu thức lambda.
Ứng dụng biểu thức lambda trong các phương thức của collection
Từ Java 8, các interface của collections có thêm những phương thức nhận functional interface — nghĩa là ta có thể truyền biểu thức lambda vào. Dưới đây là những phương thức phổ biến:
- forEach(Consumer<T> action)
- removeIf(Predicate<T> filter)
- sort(Comparator<T> c)
- replaceAll(UnaryOperator<T> operator)
Ví dụ: forEach
In tất cả phần tử của danh sách ra màn hình (cách cũ):
for (String s : list) {
System.out.println(s);
}
Bây giờ — với lambda:
list.forEach(s -> System.out.println(s));
Hoặc ngắn hơn nữa, nếu muốn “làm cao thủ Java”:
list.forEach(System.out::println); // method reference, sẽ bàn sau
Ví dụ: removeIf
Xóa tất cả chuỗi rỗng khỏi danh sách:
List<String> animals = new ArrayList<>(Arrays.asList("mèo", "", "chó", "nhím", ""));
animals.removeIf(s -> s.isEmpty());
System.out.println(animals); // [mèo, chó, nhím]
Ví dụ: sort
Sắp xếp danh sách theo độ dài chuỗi:
List<String> animals = Arrays.asList("mèo", "chó", "nhím", "voi");
animals.sort((a, b) -> a.length() - b.length());
System.out.println(animals); // [mèo, chó, voi, nhím]
Ví dụ: replaceAll
Chuyển tất cả chuỗi sang chữ hoa:
List<String> animals = new ArrayList<>(Arrays.asList("mèo", "chó", "nhím"));
animals.replaceAll(s -> s.toUpperCase());
System.out.println(animals); // [MÈO, CHÓ, NHÍM]
2. Stream API và biểu thức lambda
Từ Java 8, Stream API xuất hiện — một công cụ mạnh để xử lý collections theo phong cách hàm. Stream cho phép lọc, biến đổi, sắp xếp, thu thập collections thông qua chuỗi phương thức. Và tất cả các phương thức này nhận biểu thức lambda!
Quan trọng: Phần phân tích đầy đủ về Stream API sẽ ở sau; hiện tại — chỉ các ví dụ cơ bản để hiểu vai trò của lambda.
Ví dụ: lọc
Giữ lại các chuỗi dài hơn 3 ký tự:
List<String> animals = Arrays.asList("mèo", "voi", "nhím", "cá sấu");
animals.stream()
.filter(s -> s.length() > 3)
.forEach(System.out::println); // nhím, cá sấu
Ví dụ: biến đổi (map)
Chuyển tất cả chuỗi sang chữ hoa:
List<String> animals = Arrays.asList("mèo", "voi", "nhím");
List<String> upper = animals.stream()
.map(s -> s.toUpperCase())
.collect(Collectors.toList());
System.out.println(upper); // [MÈO, VOI, NHÍM]
Ví dụ: sắp xếp
Lấy danh sách được sắp xếp theo độ dài (không thay đổi danh sách gốc):
List<String> animals = Arrays.asList("mèo", "voi", "nhím", "cá sấu");
List<String> sorted = animals.stream()
.sorted((a, b) -> a.length() - b.length())
.collect(Collectors.toList());
System.out.println(sorted); // [mèo, voi, nhím, cá sấu]
3. So sánh với lớp ẩn danh
Hãy so sánh cùng một đoạn mã khi dùng lớp ẩn danh và khi dùng lambda.
Sắp xếp theo độ dài chuỗi
Lớp ẩn danh:
animals.sort(new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
});
Biểu thức lambda:
animals.sort((a, b) -> a.length() - b.length());
Kết luận:
Biểu thức lambda tiết kiệm rất nhiều dòng và làm mã dễ đọc hơn. Ít dấu ngoặc, ít “nhiễu” — nhiều “cốt lõi” hơn!
Xóa các chuỗi rỗng
Lớp ẩn danh:
animals.removeIf(new Predicate<String>() {
@Override
public boolean test(String s) {
return s.isEmpty();
}
});
Biểu thức lambda:
animals.removeIf(s -> s.isEmpty());
4. Thực hành: bài tập ngắn với biểu thức lambda
Hãy thử áp dụng biểu thức lambda trong một mini-program làm việc với danh sách người dùng.
Ví dụ 1: Lọc người dùng theo tuổi
class User {
String name;
int age;
User(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return name + " (" + age + ")";
}
}
List<User> users = Arrays.asList(
new User("Alisa", 17),
new User("Bob", 25),
new User("Charli", 15)
);
users.stream()
.filter(u -> u.age >= 18)
.forEach(System.out::println); // Bob (25)
Ví dụ 2: Sắp xếp người dùng theo tên
List<User> users = Arrays.asList(
new User("Alisa", 17),
new User("Bob", 25),
new User("Charli", 15)
);
users.sort((u1, u2) -> u1.name.compareTo(u2.name));
System.out.println(users);
// [Alisa (17), Bob (25), Charli (15)]
Ví dụ 3: Chuyển danh sách người dùng thành danh sách tên
List<String> names = users.stream()
.map(u -> u.name)
.collect(Collectors.toList());
System.out.println(names); // [Alisa, Bob, Charli]
Ví dụ 4: Xóa tất cả người vị thành niên
List<User> users = new ArrayList<>(Arrays.asList(
new User("Alisa", 17),
new User("Bob", 25),
new User("Charli", 15)
));
users.removeIf(u -> u.age < 18);
System.out.println(users); // [Bob (25)]
5. Những lưu ý hữu ích
Đặc điểm: phạm vi và biến “effectively final”
Biểu thức lambda có thể dùng biến từ phương thức bên ngoài, nhưng chỉ khi chúng là final hoặc “effectively final” (tức là không bị thay đổi sau khi khởi tạo).
int minAge = 18;
users.stream()
.filter(u -> u.age >= minAge)
.forEach(System.out::println);
Nếu bạn cố thay đổi minAge sau khi dùng trong lambda — trình biên dịch sẽ báo lỗi.
Bảng: các phương thức chính của collections và streams với biểu thức lambda
| Phương thức của collection/stream | Chức năng | Kiểu biểu thức lambda | Ví dụ |
|---|---|---|---|
|
Cho từng phần tử | |
|
|
Xóa phần tử theo điều kiện | |
|
|
Sắp xếp các phần tử | |
|
|
Thay thế từng phần tử | |
|
|
Lọc stream | |
|
|
Biến đổi phần tử | |
|
|
Duyệt stream | |
|
|
Sắp xếp trong stream | |
|
7. Lỗi thường gặp
Lỗi №1: Lambda quá dài. Nếu bên trong biểu thức lambda của bạn đã có 5 dòng mã, điều kiện, vòng lặp và try-catch — rất có thể nên tách đoạn mã đó ra một phương thức riêng. Lambda phù hợp cho logic ngắn gọn.
Lỗi №2: Sử dụng biến có thể thay đổi. Nếu bạn cố gắng bên trong lambda thay đổi biến từ phương thức bên ngoài (ví dụ một biến đếm), trình biên dịch sẽ không cho phép. Biến phải là final hoặc hiệu quả là final.
Lỗi №3: Quên rằng các phương thức của collection/stream không phải lúc nào cũng thay đổi collection gốc. Ví dụ, stream().filter(...) không thay đổi danh sách ban đầu, mà trả về một stream mới. Nếu muốn nhận về collection — hãy dùng collect(Collectors.toList()).
Lỗi №4: Biểu thức lambda không khớp kiểu. Nếu phương thức nhận, chẳng hạn, Comparator<T>, nhưng bạn lại truyền lambda chỉ có một tham số (thay vì hai) — sẽ xảy ra lỗi biên dịch.
Lỗi №5: Mất tính dễ đọc khi lồng nhiều lambda. Nếu bạn có một chuỗi map, filter, forEach, và bên trong mỗi lambda lại có thêm một lambda — mã sẽ trở nên khó đọc. Trong những trường hợp như vậy, tốt hơn nên tách biểu thức thành từng bước hoặc đưa một số phần vào các phương thức riêng.
GO TO FULL VERSION