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.
GO TO FULL VERSION