1. Đi sâu vào lớp ẩn danh
Lớp ẩn danh là một lớp con hoặc hiện thực interface không tên, được tạo ngay tại nơi sử dụng. Trước khi có lambda (Java 8) đây là cách thuận tiện nhất để triển khai “dùng một lần” một interface hoặc abstract class.
Ví dụ kinh điển:
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("Xin chào từ lớp ẩn danh!");
}
};
r.run();
Ở đây chúng ta khai báo và đồng thời hiện thực interface Runnable — không cần tệp riêng và tên lớp. Những hiện thực như vậy thường được dùng cho bộ xử lý sự kiện, bộ so sánh, luồng và các tác vụ khác, nơi cần “gắn” nhanh hành vi.
Nếu lambda là “biểu thức tức thời”, thì lớp ẩn danh là “diễn viên nhỏ vô danh”, đóng vai ngắn rồi biến mất.
2. So sánh với biểu thức lambda
Cú pháp
Lớp ẩn danh:
Comparator<String> comp = new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
};
Biểu thức lambda:
Comparator<String> comp = (a, b) -> a.length() - b.length();
Khác biệt rất rõ: lambda gọn hơn — không cần ghi rõ kiểu, tên phương thức và dấu ngoặc nhọn thừa nếu hành động đơn giản.
Chức năng
- Lớp ẩn danh — là một đối tượng hoàn chỉnh. Có thể khai báo trường, phương thức bổ sung, ghi đè các phương thức của Object (toString, equals, v.v.).
- Biểu thức lambda — hiện thực một phương thức trừu tượng của functional interface. Bên trong không thể khai báo trường hay phương thức riêng.
Khi nào chọn cái gì?
- Lambda — khi cần hiện thực ngắn gọn một phương thức của functional interface.
- Lớp ẩn danh — khi cần:
- hiện thực nhiều phương thức (ví dụ của abstract class);
- khai báo các trường để lưu trạng thái;
- ghi đè các phương thức Object (ví dụ toString);
- tận dụng đặc thù kế thừa/quyền truy cập (ví dụ truy cập các thành viên protected của lớp cha).
3. Phạm vi và từ khóa this
Ở đây có một bẫy phổ biến:
- trong lớp ẩn danh this tham chiếu tới thể hiện của chính lớp ẩn danh;
- trong biểu thức lambda this tham chiếu tới lớp bên ngoài nơi khai báo lambda.
Ví dụ: so sánh hành vi
public class Outer {
String name = "Lớp bên ngoài";
void test() {
Runnable anon = new Runnable() {
String name = "Lớp ẩn danh";
@Override
public void run() {
System.out.println(this.name); // "Lớp ẩn danh"
}
};
Runnable lambda = () -> System.out.println(this.name); // "Lớp bên ngoài"
anon.run();
lambda.run();
}
}
Kết quả:
Lớp ẩn danh
Lớp bên ngoài
Trong lớp ẩn danh, this trỏ tới chính lớp ẩn danh (lấy trường name của nó). Với lambda, this là Outer.
4. Khi nào dùng lớp ẩn danh?
Nếu cần hiện thực nhiều hơn một phương thức
Lambda chỉ hoạt động với functional interface (chính xác một phương thức trừu tượng). Nếu interface/abstract class yêu cầu hiện thực nhiều phương thức — cần dùng lớp ẩn danh.
abstract class Animal {
abstract void say();
abstract void jump();
}
Animal cat = new Animal() {
@Override
void say() {
System.out.println("Meo!");
}
@Override
void jump() {
System.out.println("Nhảy!");
}
};
Nếu cần lưu trạng thái (các trường)
Runnable r = new Runnable() {
int counter = 0;
@Override
public void run() {
counter++;
System.out.println("Được gọi " + counter + " lần");
}
};
r.run(); // Được gọi 1 lần
r.run(); // Được gọi 2 lần
Nếu cần ghi đè các phương thức của Object
Comparator<String> comp = new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
@Override
public String toString() {
return "Bộ so sánh theo độ dài chuỗi";
}
};
System.out.println(comp); // Bộ so sánh theo độ dài chuỗi
5. Ví dụ: Comparator và Runnable — lambda vs lớp ẩn danh
Sắp xếp chuỗi theo độ dài
Lớp ẩn danh:
List<String> words = Arrays.asList("mèo", "chuột", "hươu", "lợn");
words.sort(new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
});
System.out.println(words);
Biểu thức lambda:
List<String> words = Arrays.asList("mèo", "chuột", "hươu", "lợn");
words.sort((a, b) -> a.length() - b.length());
System.out.println(words);
Kết quả như nhau, nhưng mã với lambda ngắn gọn và dễ đọc hơn.
Runnable: khởi chạy luồng
Lớp ẩn danh:
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Luồng qua lớp ẩn danh");
}
});
t1.start();
Biểu thức lambda:
Thread t2 = new Thread(() -> System.out.println("Luồng qua lambda"));
t2.start();
Lớp ẩn danh có trường
Runnable r = new Runnable() {
int count = 0;
@Override
public void run() {
count++;
System.out.println("Được gọi " + count + " lần");
}
};
r.run(); // Được gọi 1 lần
r.run(); // Được gọi 2 lần
Với lambda thì không — không thể khai báo trường.
6. Đặc điểm: phạm vi, biến và final
Cả trong lớp ẩn danh và trong biểu thức lambda, các biến cục bộ của phương thức bên ngoài chỉ có thể dùng nếu chúng là final hoặc “effectively final” (không thay đổi sau khi khởi tạo). Nhưng có khác biệt về tên:
- trong lớp ẩn danh có thể khai báo biến trùng tên với biến ở phạm vi ngoài (shadowing/“che khuất”);
- trong lambda — không thể: tên không được xung đột với tên biến bên ngoài.
Ví dụ:
int x = 10;
Runnable r = new Runnable() {
@Override
public void run() {
int x = 20; // OK: che khuất (shadow) biến bên ngoài
System.out.println(x); // 20
}
};
r.run();
Runnable l = () -> {
// int x = 30; // Lỗi biên dịch: biến đã được định nghĩa
System.out.println(x); // 10
};
l.run();
7. Khi nào lambda tốt hơn, và khi nào lớp ẩn danh là không thể thay thế?
Biểu thức lambda — lựa chọn của bạn nếu:
- cần hiện thực ngắn một hàm cho functional interface;
- không cần lưu trạng thái;
- không cần ghi đè các phương thức của Object;
- việc sử dụng mang tính “ngay và luôn” và đơn giản.
Lớp ẩn danh — cần thiết nếu:
- cần hiện thực interface với nhiều phương thức hoặc một abstract class;
- cần khai báo các trường hoặc phương thức bổ sung;
- cần ghi đè toString, equals, hashCode;
- cần truy cập các thành viên protected của lớp cha.
8. Thực hành: so sánh qua ví dụ
Bài 1: Lọc danh sách bằng Predicate
Lớp ẩn danh:
List<String> animals = Arrays.asList("mèo", "chuột", "hươu", "lợn");
animals.removeIf(new Predicate<String>() {
@Override
public boolean test(String s) {
return s.length() < 4;
}
});
System.out.println(animals); // [chuột, hươu, lợn]
Biểu thức lambda:
List<String> animals = Arrays.asList("mèo", "chuột", "hươu", "lợn");
animals.removeIf(s -> s.length() < 4);
System.out.println(animals); // [chuột, hươu, lợn]
Bài 2: So sánh phạm vi của this
public class Demo {
String name = "Demo";
void check() {
Runnable anon = new Runnable() {
String name = "Anon";
@Override
public void run() {
System.out.println(this.name); // "Anon"
}
};
Runnable lambda = () -> System.out.println(this.name); // "Demo"
anon.run();
lambda.run();
}
public static void main(String[] args) {
new Demo().check();
}
}
9. Những lỗi thường gặp khi làm việc với lớp ẩn danh và biểu thức lambda
Lỗi số 1: Kỳ vọng rằng lambda có thể hiện thực nhiều phương thức. Lambda chỉ làm việc với functional interface (một phương thức trừu tượng). Nếu có nhiều phương thức hơn — hãy dùng lớp ẩn danh.
Lỗi số 2: Nhầm lẫn về phạm vi của this. Trong lambda, this là lớp bên ngoài, còn trong lớp ẩn danh — là chính lớp ẩn danh. Vì thế rất dễ lấy “sai” trường và giá trị.
Lỗi số 3: Cố gắng khai báo trường trong lambda. Trong lambda không thể khai báo trường riêng — chỉ có thể dùng biến của ngữ cảnh bên ngoài (final/“effectively final”). Nếu cần trạng thái, hãy dùng lớp ẩn danh.
Lỗi số 4: Che khuất (shadow) biến. Trong lớp ẩn danh có thể khai báo biến cục bộ trùng tên với biến ở phạm vi ngoài — đó là che khuất. Với lambda thì không: trình biên dịch sẽ báo lỗi.
Lỗi số 5: Đưa quá nhiều logic vào lambda. Nếu thân lambda dài hơn 3–5 dòng, khả năng đọc sẽ giảm. Tốt hơn nên tách mã ra một phương thức riêng hoặc dùng lớp ẩn danh (nếu cần trạng thái/nhiều phương thức).
GO TO FULL VERSION