CodeGym /Các khóa học /JAVA 25 SELF /Truyền hàm như tham số: ví dụ

Truyền hàm như tham số: ví dụ

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

1. Truyền hành vi thay vì dữ liệu

Vì sao lại cần truyền hàm như một tham số?

Hãy tưởng tượng: bạn có một phương thức để sắp xếp danh sách. Nhưng làm sao nó biết sắp xếp theo tiêu chí nào? Theo bảng chữ cái? Theo độ dài? Theo ngày sinh? Bạn có thể viết một phương thức riêng cho từng trường hợp, nhưng nó sẽ nhanh chóng biến thành một mớ “copy-paste” khủng khiếp.

Thay vào đó, Java cho phép truyền hành vi (nghĩa là một hàm hoặc lambda) để xác định cách sắp xếp, lọc, biến đổi, v.v. Điều này làm cho mã linh hoạt và có thể tái sử dụng.

Ví dụ: sắp xếp với comparator

List<String> names = List.of("Anna", "Boris", "Vika");

// Truyền hành vi: so sánh phần tử như thế nào (theo độ dài chuỗi)
names.stream()
     .sorted((a, b) -> a.length() - b.length())
     .forEach(System.out::println);

Ví dụ: lọc với predicate

List<String> names = List.of("Anna", "Boris", "Vika");

// Truyền hành vi: giữ ai lại (tên dài hơn 4 ký tự)
names.stream()
     .filter(name -> name.length() > 4)
     .forEach(System.out::println);

Trong cả hai trường hợp, bạn không “nhúng cứng” logic vào phương thức, mà đưa cho nó một phần hành vi để áp dụng lên từng phần tử.

Lợi ích: ít lặp lại hơn, linh hoạt hơn

Thay vì hàng chục phương thức với mã tương tự nhưng logic khác nhau, bạn viết một phương thức tổng quát nhận “cần làm gì” dưới dạng hàm. Điều này tiết kiệm thời gian, giảm lỗi và giúp mã dễ kiểm thử hơn.

2. Cú pháp truyền hàm

Biểu thức lambda làm đối số

Cách thường gặp nhất — viết trực tiếp lambda với mũi tên -> ngay tại nơi gọi phương thức:

list.forEach(item -> System.out.println(item));

Hoặc còn ngắn hơn nếu đã có sẵn phương thức phù hợp — dùng tham chiếu phương thức (::):

list.forEach(System.out::println);

Tham chiếu phương thức (method references)

Nếu bạn đã có phương thức phù hợp, có thể truyền nó dưới dạng tham chiếu :::

// Phương thức thông thường
public static boolean isLongName(String name) {
    return name.length() > 4;
}

// Truyền tham chiếu đến phương thức
names.stream()
     .filter(MyClass::isLongName)
     .forEach(System.out::println);

Điều này hoạt động khi chữ ký phương thức khớp với cái được kỳ vọng trong functional interface (Predicate<T>, Function<T, R>, Comparator<T>, v.v.).

3. Ví dụ từ thư viện chuẩn

Collections.sort và Comparator

List<String> names = new ArrayList<>(List.of("Anna", "Boris", "Vika"));

// Sắp xếp theo độ dài tên
names.sort((a, b) -> a.length() - b.length());
System.out.println(names); // [Vika, Anna, Boris]

Stream.filter và Predicate

List<String> names = List.of("Anna", "Boris", "Vika");

// Chỉ giữ các tên bắt đầu bằng 'V'
names.stream()
     .filter(name -> name.startsWith("V"))
     .forEach(System.out::println); // Vika

Stream.map và Function

List<String> names = List.of("Anna", "Boris", "Vika");

// Chuyển tên sang chữ hoa
names.stream()
     .map(String::toUpperCase)
     .forEach(System.out::println);

Optional.ifPresent và Consumer

Optional<String> opt = Optional.of("Xin chào!");

// Nếu có giá trị thì in ra
opt.ifPresent(s -> System.out.println("Trong chuỗi: " + s));

4. Thực hành: viết các phương thức của riêng bạn với hàm là tham số

Đến lúc áp dụng kiến thức vào thực tế! Hãy viết vài phương thức nhận một hàm làm tham số và sử dụng nó bên trong.

Ví dụ 1: Phương thức xử lý phần tử của danh sách (forEach kiểu riêng)

Giả sử ta có danh sách người dùng (User). Ta muốn thực hiện một hành động nào đó trên mỗi người dùng — ví dụ in tên, gửi e-mail, cộng thưởng, v.v. Thay vì “nhúng cứng” hành động, ta truyền nó như một tham số — Consumer<User>!

import java.util.List;
import java.util.function.Consumer;

class User {
    String name;
    int age;
    User(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

public class UserProcessor {
    // Phương thức nhận Consumer<User>
    public static void processUsers(List<User> users, Consumer<User> action) {
        for (User user : users) {
            action.accept(user);
        }
    }

    public static void main(String[] args) {
        List<User> users = List.of(
            new User("Anna", 25),
            new User("Boris", 30),
            new User("Vika", 22)
        );

        // In tên của tất cả người dùng
        processUsers(users, user -> System.out.println("Tên: " + user.name));

        // Cộng thưởng (ví dụ này chỉ in ra)
        processUsers(users, user -> System.out.println(user.name + " đã nhận thưởng!"));
    }
}

Ví dụ 2: Phương thức lọc phần tử (Predicate)

Viết một phương thức trả về chỉ những người dùng thỏa điều kiện (ví dụ chỉ người trưởng thành):

import java.util.List;
import java.util.ArrayList;
import java.util.function.Predicate;

public class UserFilter {
    public static List<User> filterUsers(List<User> users, Predicate<User> condition) {
        List<User> result = new ArrayList<>();
        for (User user : users) {
            if (condition.test(user)) {
                result.add(user);
            }
        }
        return result;
    }

    public static void main(String[] args) {
        List<User> users = List.of(
            new User("Anna", 25),
            new User("Boris", 17),
            new User("Vika", 22)
        );

        // Lọc chỉ người trưởng thành
        List<User> adults = filterUsers(users, user -> user.age >= 18);
        adults.forEach(user -> System.out.println(user.name)); // Anna, Vika
    }
}

Ví dụ 3: Phương thức chuyển đổi (Function)

Bây giờ là phương thức chuyển danh sách người dùng thành danh sách tên của họ (hoặc mọi biến đổi khác):

import java.util.List;
import java.util.ArrayList;
import java.util.function.Function;

public class UserMapper {
    public static <R> List<R> mapUsers(List<User> users, Function<User, R> mapper) {
        List<R> result = new ArrayList<>();
        for (User user : users) {
            result.add(mapper.apply(user));
        }
        return result;
    }

    public static void main(String[] args) {
        List<User> users = List.of(
            new User("Anna", 25),
            new User("Boris", 17),
            new User("Vika", 22)
        );

        // Lấy danh sách tên
        List<String> names = mapUsers(users, user -> user.name);
        System.out.println(names); // [Anna, Boris, Vika]

        // Lấy danh sách tuổi
        List<Integer> ages = mapUsers(users, user -> user.age);
        System.out.println(ages); // [25, 17, 22]
    }
}

Ví dụ 4: Phương thức sinh giá trị (Supplier)

Đôi khi cần lấy một giá trị “khi cần” — ví dụ tạo số ngẫu nhiên, tạo đối tượng, lấy thời gian hiện tại. Khi đó dùng giao diện Supplier<T>.

import java.util.function.Supplier;

public class ValueGenerator {
    public static int getValue(Supplier<Integer> supplier) {
        return supplier.get();
    }

    public static void main(String[] args) {
        // Lấy số ngẫu nhiên
        int random = getValue(() -> (int)(Math.random() * 100));
        System.out.println("Số ngẫu nhiên: " + random);

        // Lấy thời gian hiện tại theo mili giây
        long time = getValue(System::currentTimeMillis);
        System.out.println("Thời gian: " + time);
    }
}

5. Ứng dụng thống nhất: liên kết các ví dụ

Hãy tưởng tượng chúng ta phát triển một ứng dụng đơn giản “Danh sách người dùng”. Chúng ta đã biết:

  • Lọc người dùng theo điều kiện (ví dụ chỉ người trưởng thành);
  • Biến đổi người dùng thành thứ khác (tên, e-mail, tuổi);
  • Thực hiện hành động trên mỗi người dùng (in, cộng thưởng);
  • Sinh giá trị theo yêu cầu (ví dụ tạo người dùng mới).

Giờ đây, dùng tất cả các phương thức này, ta có thể xây dựng các kịch bản xử lý dữ liệu linh hoạt mà không phải viết lại mã cho mỗi bài toán mới.

import java.util.*;
import java.util.function.*;

public class UserApp {
    static class User {
        String name;
        int age;
        User(String name, int age) {
            this.name = name;
            this.age = age;
        }
        public String toString() {
            return name + " (" + age + ")";
        }
    }

    // Phương thức tổng quát để xử lý người dùng
    static void processUsers(List<User> users, Consumer<User> action) {
        for (User user : users) action.accept(user);
    }

    // Bộ lọc tổng quát
    static List<User> filterUsers(List<User> users, Predicate<User> condition) {
        List<User> result = new ArrayList<>();
        for (User user : users) if (condition.test(user)) result.add(user);
        return result;
    }

    // Bộ chuyển đổi tổng quát
    static <R> List<R> mapUsers(List<User> users, Function<User, R> mapper) {
        List<R> result = new ArrayList<>();
        for (User user : users) result.add(mapper.apply(user));
        return result;
    }

    public static void main(String[] args) {
        List<User> users = List.of(
            new User("Anna", 25),
            new User("Boris", 17),
            new User("Vika", 22)
        );

        // 1. In tất cả người dùng
        processUsers(users, user -> System.out.println("Người dùng: " + user));

        // 2. Chỉ tìm người lớn
        List<User> adults = filterUsers(users, user -> user.age >= 18);
        System.out.println("Người trưởng thành: " + adults);

        // 3. Lấy tên của người trưởng thành
        List<String> adultNames = mapUsers(adults, user -> user.name);
        System.out.println("Tên của người trưởng thành: " + adultNames);

        // 4. Cộng thưởng cho người trưởng thành
        processUsers(adults, user -> System.out.println(user.name + " đã nhận thưởng!"));
    }
}

Tại sao cách này tốt hơn “cách thông thường”?

Cách tiếp cận này mang lại tính linh hoạt: cùng một bộ phương thức có thể dùng trong các kịch bản rất khác nhau chỉ bằng cách truyền các hàm khác nhau. Mã không phình to thành vô số biến thể “lọc kiểu này”, “in kiểu kia” — chúng ta có các công cụ dùng chung hoạt động ở mọi nơi. Nhờ vậy mã gọn hơn, dễ hiểu hơn và ít lỗi hơn. Ngoài ra, phong cách này hòa hợp rất tốt với Stream API hiện đại, nên khi chuyển sang luồng bạn không phải học lại — cách tiếp cận vẫn như vậy.

6. Các lỗi thường gặp khi truyền hàm như tham số

Lỗi 1: Chữ ký không khớp với interface kỳ vọng.
Nếu phương thức nhận Predicate<User> nhưng bạn truyền một lambda trả về không phải boolean mà, ví dụ, là String, trình biên dịch sẽ lập tức báo lỗi và không cho build. Hãy kiểm tra giá trị trả về và tham số khớp với cái được kỳ vọng.

Lỗi 2: Lambda sử dụng biến có thể thay đổi.
Trong Java, biểu thức lambda chỉ có thể sử dụng các biến final hoặc effectively final từ ngữ cảnh bên ngoài. Nếu bạn cố thay đổi biến như vậy bên trong lambda — sẽ có lỗi biên dịch.

Lỗi 3: Nhầm lẫn giữa các interface.
Đôi khi bạn muốn truyền Consumer<T>, nhưng lại lỡ viết một hàm có trả về gì đó (ví dụ Function<T, R>). Hãy kiểm tra lambda của bạn trả về đúng thứ cần thiết (hoặc không trả về gì — đối với Consumer).

Lỗi 4: Lambda quá phức tạp ngay trong tham số.
Nếu một lambda dài hơn một hai dòng, tốt hơn nên tách nó ra thành biến hoặc phương thức riêng. Nếu không, mã sẽ khó đọc và có thể ngay cả bạn cũng khó mà hiểu sau này.

1
Nhiệm vụ
JAVA 25 SELF, mức độ, bài học
Đã khóa
Diễu hành thú vật: Xếp theo độ dài tên gọi 🐘
Diễu hành thú vật: Xếp theo độ dài tên gọi 🐘
1
Nhiệm vụ
JAVA 25 SELF, mức độ, bài học
Đã khóa
Báo cáo về chỉ số "Giám sát sao" 🔭
Báo cáo về chỉ số "Giám sát sao" 🔭
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION