1. Đi sâu vào interface hàm
Interface hàm là interface có CHỈ MỘT phương thức trừu tượng (tức là chưa được hiện thực). Nhờ vậy Java hiểu: “à, có thể truyền lambda vào đây!” hoặc tham chiếu phương thức.
Để tránh nhầm lẫn, trong các interface như vậy thường có annotation @FunctionalInterface. Nó không bắt buộc, nhưng nếu bạn thêm nó và vô tình viết phương thức trừu tượng thứ hai, trình biên dịch sẽ lập tức báo lỗi.
Ví dụ:
@FunctionalInterface
interface MyAction {
void run();
}
MyAction action = () -> System.out.println("Xin chào từ lambda!");
action.run(); // Sẽ in: Xin chào từ lambda!
Để làm gì?
- Cho phép dùng biểu thức lambda và tham chiếu phương thức thay vì tạo lớp ẩn danh (ít boilerplate hơn!).
- Giúp trình biên dịch hiểu rằng interface này được thiết kế cho lập trình hàm.
Thông tin thú vị: Thư viện chuẩn Java đã có hàng chục interface như vậy — không cần phát minh lại bánh xe!
2. Tổng quan các interface hàm chuẩn
Trong gói java.util.function có hàng chục interface hàm. Hãy xem bốn interface phổ biến nhất (chúng được dùng “nhiều nhất” trong số các interface Java).
| Interface | Nhận gì | Trả về gì | Dùng để làm gì |
|---|---|---|---|
|
|
|
Kiểm tra điều kiện (lọc) |
|
|
|
Thực hiện hành động trên đối tượng |
|
không gì | |
Lấy/tạo đối tượng |
|
|
|
Chuyển đổi T thành R |
Predicate<T>
Mô tả: Hàm nhận một đối tượng kiểu T và trả về true hoặc false. Ví dụ điển hình: lọc danh sách. Phương thức chính — test.
Predicate<String> isLong = s -> s.length() > 5;
System.out.println(isLong.test("Java")); // false
System.out.println(isLong.test("Functional")); // true
Consumer<T>
Mô tả: Nhận một đối tượng kiểu T và thực hiện hành động với nó, không trả về gì. Phương thức chính — accept.
Consumer<String> printer = s -> System.out.println("In: " + s);
printer.accept("Hello, world!"); // In: Hello, world!
Supplier<T>
Mô tả: Không nhận gì, trả về một đối tượng kiểu T. Có thể xem như “bộ tạo” giá trị. Phương thức chính — get.
Supplier<Double> randomSupplier = () -> Math.random();
System.out.println(randomSupplier.get()); // Ví dụ, 0.1234567
Function<T, R>
Mô tả: Nhận một đối tượng kiểu T và trả về một đối tượng kiểu R. Ví dụ điển hình: chuyển đổi dữ liệu. Phương thức chính — apply.
Function<String, Integer> stringToLength = s -> s.length();
System.out.println(stringToLength.apply("Java")); // 4
Ngắn gọn: UnaryOperator, BinaryOperator, BiFunction
- UnaryOperator<T> — tương đương Function<T, T>: nhận và trả về cùng một kiểu.
- BinaryOperator<T> — tương đương BiFunction<T, T, T>: nhận hai T, trả về một T.
- BiFunction<T, U, R> — nhận hai kiểu khác nhau, trả về kiểu thứ ba.
UnaryOperator<Integer> square = x -> x * x;
BinaryOperator<Integer> sum = (a, b) -> a + b;
BiFunction<String, Integer, String> repeat = (s, n) -> s.repeat(n);
3. Ví dụ sử dụng
Hãy xem các interface này xuất hiện trong bài toán thực tế như thế nào, đặc biệt là trong collections và Stream API.
Truyền vào các phương thức của Collections và Stream API
Ví dụ 1: Predicate và lọc
List<String> words = List.of("java", "stream", "lambda", "code");
List<String> longWords = words.stream()
.filter(word -> word.length() > 4) // Predicate<String>
.toList();
System.out.println(longWords); // [stream, lambda]
Ví dụ 2: Consumer và forEach
words.forEach(word -> System.out.println("Từ: " + word)); // Consumer<String>
Ví dụ 3: Function và map
List<Integer> lengths = words.stream()
.map(word -> word.length()) // Function<String, Integer>
.toList();
System.out.println(lengths); // [4, 6, 6, 4]
Ví dụ 4: Supplier và tạo giá trị
Supplier<String> greetingSupplier = () -> "Xin chào, Java!";
System.out.println(greetingSupplier.get()); // Xin chào, Java!
So sánh với lớp ẩn danh
Trước đây phải viết như thế này:
Predicate<String> isShort = new Predicate<String>() {
@Override
public boolean test(String s) {
return s.length() < 5;
}
};
Với lambda thì dễ chịu hơn nhiều:
Predicate<String> isShort = s -> s.length() < 5;
4. Thực hành: viết biểu thức lambda cho từng interface
Ta sẽ hiện thực một ứng dụng nhỏ — danh sách người dùng. Mỗi người dùng được biểu diễn bằng lớp User:
public class User {
private final String name;
private final int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
@Override
public String toString() {
return name + " (" + age + ")";
}
}
Tạo danh sách người dùng:
List<User> users = List.of(
new User("Anna", 23),
new User("Boris", 17),
new User("Vika", 31),
new User("Gosha", 15)
);
Predicate: lọc người trưởng thành
Predicate<User> isAdult = user -> user.getAge() >= 18;
List<User> adults = users.stream()
.filter(isAdult)
.toList();
System.out.println("Người lớn: " + adults); // Người lớn: [Anna (23), Vika (31)]
Consumer: in người dùng
Consumer<User> printUser = user -> System.out.println("Người dùng: " + user);
adults.forEach(printUser);
Supplier: tạo người dùng
Supplier<User> randomUserSupplier = () -> {
String[] names = {"Dima", "Katya", "Lyosha"};
int randomAge = 10 + (int)(Math.random() * 30);
String randomName = names[(int)(Math.random() * names.length)];
return new User(randomName, randomAge);
};
User randomUser = randomUserSupplier.get();
System.out.println("Người dùng ngẫu nhiên: " + randomUser);
Function: lấy tên người dùng
Function<User, String> getName = user -> user.getName();
List<String> names = users.stream()
.map(getName)
.toList();
System.out.println("Tên: " + names); // Tên: [Anna, Boris, Vika, Gosha]
5. Những điểm hữu ích
Sử dụng trong Stream API: filter, map, forEach, v.v.
Gom tất cả lại và viết một chuỗi biến đổi:
users.stream()
.filter(user -> user.getAge() >= 18) // Predicate<User>
.map(user -> user.getName().toUpperCase()) // Function<User, String>
.forEach(name -> System.out.println("Người lớn: " + name)); // Consumer<String>
Kết quả:
Người lớn: ANNA
Người lớn: VIKA
Bảng ghi nhớ: dùng cái gì ở đâu
| Dùng ở đâu | Cần interface nào | Ví dụ sử dụng |
|---|---|---|
| filter (Stream) | |
|
| map (Stream) | |
|
| forEach (Stream, List) | |
|
| generate (Stream) | |
|
Vì sao cần biết về các interface hàm?
- Chúng là nền tảng của mọi biểu thức lambda trong Java.
- Giúp viết code tổng quát, tái sử dụng và súc tích.
- Đơn giản hóa công việc với collections, streams, tác vụ bất đồng bộ.
Khi nào dùng interface nào?
- Predicate — khi cần kiểm tra điều kiện (lọc, tìm kiếm).
- Consumer — khi cần làm gì đó với đối tượng (in ra, ghi, gửi).
- Supplier — khi cần lấy hoặc tạo đối tượng (factory, generator).
- Function — khi cần chuyển đổi đối tượng từ kiểu này sang kiểu khác.
6. Những lỗi thường gặp khi làm việc với interface hàm
Lỗi số 1: Chọn sai interface. Đôi khi người mới nhầm Predicate với Function — ví dụ, cố trả về boolean từ Function thay vì từ Predicate. Hãy nhớ: Predicate luôn trả về boolean, còn Function — là bất kỳ kiểu nào khác.
Lỗi số 2: Không dùng interface chuẩn. Thường viết interface tự chế kiểu “Checker” với phương thức boolean check(T t) thay vì dùng Predicate. Tốt hơn nên dùng chuẩn — chúng được hỗ trợ ở mọi nơi và làm code dễ hiểu hơn với người khác.
Lỗi số 3: Lambda quá phức tạp. Nếu một lambda biến thành một đoạn 10 dòng, hãy tách nó ra thành phương thức hoặc lớp riêng. Lambda hướng đến ngắn gọn và dễ đọc.
Lỗi số 4: Quên annotation @FunctionalInterface. Nếu bạn viết interface hàm của riêng mình — đừng quên annotation này. Nó giúp phòng lỗi (như thêm nhầm phương thức trừu tượng thứ hai).
Lỗi số 5: Dùng trạng thái có thể thay đổi bên trong lambda. Nếu lambda thay đổi biến hoặc collection bên ngoài, điều đó có thể dẫn đến lỗi khó lường, đặc biệt khi làm việc với luồng. Tốt nhất tránh hiệu ứng phụ.
GO TO FULL VERSION