CodeGym /Các khóa học /JAVA 25 SELF /Các interface hàm: Predicate, Consumer, Supplier, Functio...

Các interface hàm: Predicate, Consumer, Supplier, Function

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

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ì
Predicate<T>
T
boolean
Kiểm tra điều kiện (lọc)
Consumer<T>
T
void
Thực hiện hành động trên đối tượng
Supplier<T>
không gì
T
Lấy/tạo đối tượng
Function<T, R>
T
R
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)
Predicate<T>
filter(u -> u.getAge() > 18)
map (Stream)
Function<T, R>
map(u -> u.getName())
forEach (Stream, List)
Consumer<T>
forEach(u -> System.out.println(u))
generate (Stream)
Supplier<T>
Stream.generate(() -> ...)

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ụ.

1
Nhiệm vụ
JAVA 25 SELF, mức độ, bài học
Đã khóa
Phân tích tiêu đề sách trong Thư viện Khoa học Viễn tưởng 📚
Phân tích tiêu đề sách trong Thư viện Khoa học Viễn tưởng 📚
1
Nhiệm vụ
JAVA 25 SELF, mức độ, bài học
Đã khóa
Quản lý các chỉ số trò chơi trong "Vương quốc Ảo" 🎮
Quản lý các chỉ số trò chơi trong "Vương quốc Ảo" 🎮
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION