99 Ngày Java — Ngày 52: Functional interface
Hôm qua ta viết được lambda, nhưng còn một câu hỏi chưa trả lời: lambda đó có kiểu gì? Java không có kiểu hàm như nhiều ngôn ngữ khác, nên mỗi lambda buộc phải "đội lốt" một interface. Hôm nay là bộ interface cả ngành dùng chung — bốn cái chính, và bạn phân biệt chúng bằng đúng hai câu hỏi: có nhận gì vào không, có trả gì ra không.

Bốn interface, phân biệt bằng hai câu hỏi
Đừng học thuộc bảng. Chỉ cần hỏi hai câu: nó nhận gì vào và nó trả gì ra. Nhận vào có trả ra là Function; nhận vào không trả ra là Consumer; không nhận mà có trả là Supplier; trả về boolean là Predicate. Xong.
// BỐN INTERFACE — nhớ được bốn cái này là đọc được 90% code Java hiện đại.
// Tất cả nằm trong java.util.function, và phân biệt nhau bằng ĐÚNG HAI câu hỏi:
// ① Nó có NHẬN gì vào không? ② Nó có TRẢ gì ra không?
// nhận trả method dùng khi
Function<T,R> // T R apply() BIẾN ĐỔI: từ cái này ra cái kia
Consumer<T> // T — accept() LÀM GÌ ĐÓ rồi thôi (in, lưu, gửi)
Supplier<T> // — T get() SINH RA giá trị (không cần đầu vào)
Predicate<T> // T boolean test() HỎI ĐÚNG/SAI (lọc, kiểm tra)
Function<String, Integer> doDai = s -> s.length();
Consumer<String> in = s -> System.out.println(s);
Supplier<LocalDate> homNay = () -> LocalDate.now();
Predicate<String> rong = s -> s.isEmpty();
// ⭐ VÌ SAO PHẢI CÓ BỐN CÁI RIÊNG mà không dùng một cái chung?
// Vì Java KHÔNG có kiểu hàm như các ngôn ngữ khác. Một lambda BẮT BUỘC
// phải có một interface để "đội lốt". Bốn interface này là bốn hình dạng
// phổ biến nhất, được đưa sẵn vào thư viện chuẩn để cả ngành dùng chung tên gọi.
// ❗ TRÁNH BOXING với kiểu nguyên thủy — đây là lý do có cả một họ biến thể:
Function<Integer, Integer> cham = x -> x * 2; // ❌ mỗi lần gọi: unbox → tính → box lại
IntUnaryOperator nhanh = x -> x * 2; // ✅ int thuần, không sinh rác
// Còn có IntPredicate, ToIntFunction, IntSupplier... Với vòng lặp triệu phần tử,
// khác biệt này là thật, không phải tối ưu vặt.
// BIẾN THỂ HAI THAM SỐ — khi một tham số là không đủ:
BiFunction<String, Integer, String> lap = (s, n) -> s.repeat(n);
BinaryOperator<Integer> cong = (a, b) -> a + b; // BiFunction<T,T,T> viết gọn
UnaryOperator<String> hoa = s -> s.toUpperCase(); // Function<T,T> viết gọnJava không có kiểu hàm, nên lambda buộc phải có interface để đội lốt — bốn cái này là bốn hình dạng phổ biến nhất, đưa sẵn vào thư viện chuẩn.
Biến thể nguyên thủy tồn tại để tránh boxing:
IntUnaryOperatorkhông sinh rác, cònFunction<Integer,Integer>thì unbox rồi box lại mỗi lần gọi.BiFunctioncho hai tham số;UnaryOperatorvàBinaryOperatorlà cách viết gọn khi các kiểu trùng nhau.
Ghép hàm — và giá trị thật nằm ở cái tên
andThen và compose cho cùng kết quả nhưng đọc ngược chiều nhau. Lời khuyên thực dụng: chỉ dùng andThen, vì nó đọc theo đúng thứ tự xảy ra. Nhưng thứ đáng giá nhất trong phần này không phải cú pháp ghép — mà là việc điều kiện lọc có tên và tái sử dụng được.
// GHÉP HÀM — chỗ mấy interface này thật sự phát huy sức mạnh.
Function<String, String> bo = String::trim;
Function<String, String> hoa = String::toUpperCase;
// andThen: LÀM CÁI NÀY TRƯỚC, rồi mới tới cái kia (đọc từ TRÁI sang PHẢI)
Function<String, String> lam1 = bo.andThen(hoa); // trim rồi mới upper
// compose: NGƯỢC LẠI — cái trong ngoặc chạy TRƯỚC (đọc từ PHẢI sang TRÁI)
Function<String, String> lam2 = hoa.compose(bo); // cũng là trim rồi upper
// ❗ HAI CÁI TRÊN CHO KẾT QUẢ GIỐNG NHAU, nhưng đọc ngược chiều nhau.
// Lời khuyên thực dụng: CHỈ DÙNG andThen. Nó đọc theo thứ tự xảy ra,
// và người đọc code không phải dừng lại để đảo ngược trong đầu.
// PREDICATE ghép bằng and / or / negate — thay cho một rừng if lồng nhau:
Predicate<DonHang> daTra = d -> d.trangThai() == DA_TRA;
Predicate<DonHang> lonHon1M = d -> d.tong().compareTo(MOT_TRIEU) > 0;
var donVip = daTra.and(lonHon1M); // ⭐ đọc gần như tiếng Việt
var chuaTra = daTra.negate();
danhSach.stream().filter(donVip).toList();
// ⭐ GIÁ TRỊ THẬT KHÔNG PHẢI Ở CÚ PHÁP, MÀ Ở CHỖ ĐẶT TÊN:
// Điều kiện lọc giờ có TÊN và TÁI SỬ DỤNG được, thay vì một biểu thức
// dài lặp lại ở năm chỗ khác nhau trong dự án.
// CONSUMER cũng ghép được — chạy TUẦN TỰ, không phải thay thế nhau:
Consumer<DonHang> ghiLog = d -> log.info("Xử lý {}", d.id());
Consumer<DonHang> guiMail = d -> email.gui(d);
ghiLog.andThen(guiMail).accept(don); // cả hai đều chạy, theo đúng thứ tự
// ❗ Nếu ghiLog ném ngoại lệ thì guiMail KHÔNG chạy — chuỗi dừng ngay tại đó.Predicateghép bằngand,or,negate— thay cho một rừngiflồng nhau, và đọc gần như tiếng Việt.Đặt tên cho điều kiện quan trọng hơn việc ghép:
donVipnói rõ ý định, còn một biểu thức dài lặp ở năm chỗ thì không.Consumer.andThenchạy tuần tự cả hai, không phải thay thế nhau — nhưng nếu cái đầu ném ngoại lệ thì cái sau không chạy.
Khi nào nên tự viết interface
Đừng viết lại Predicate dưới một cái tên khác — dùng tên chuẩn thì người mới vào dự án hiểu ngay. Nhưng có ba trường hợp tự viết là đúng: khi cái tên mang ý nghĩa nghiệp vụ, khi cần ném checked exception, và khi muốn đặt tên cho một quy tắc hay thay đổi.
// TỰ VIẾT FUNCTIONAL INTERFACE — khi nào nên, và khi nào đừng.
// ❌ ĐỪNG viết lại thứ đã có tên trong thư viện chuẩn:
@FunctionalInterface
interface KiemTraChuoi { boolean kiem(String s); } // đây chính là Predicate<String>
// Dùng lại tên chuẩn thì người mới vào dự án hiểu ngay, khỏi đọc thêm.
// ✅ NÊN viết khi cái tên mang Ý NGHĨA NGHIỆP VỤ:
@FunctionalInterface
interface ChinhSachGiamGia {
BigDecimal tinh(DonHang don, KhachHang khach);
}
// So sánh hai cách khai báo cùng một thứ:
BiFunction<DonHang, KhachHang, BigDecimal> cs; // đúng kiểu, nhưng vô hồn
ChinhSachGiamGia cs; // ⭐ đọc là hiểu ngay nó làm gì
// Với người đọc code sáu tháng sau, khác biệt này lớn hơn bạn tưởng.
// ✅ NÊN viết khi cần NÉM CHECKED EXCEPTION — đây là hạn chế thật của bộ chuẩn:
list.forEach(f -> Files.delete(f)); // ❌ Files.delete ném IOException (checked)
// mà Consumer.accept KHÔNG khai ném gì cả
@FunctionalInterface
interface ConsumerNemDuoc<T> { void accept(T t) throws Exception; }
// ⭐ ĐÂY LÀ ĐIỂM ĐAU LỚN NHẤT của lập trình hàm trong Java: toàn bộ
// java.util.function KHÔNG khai ném checked exception. Nên trong stream,
// bạn hoặc bọc lại thành unchecked, hoặc tự định nghĩa interface như trên.
// (Ngày 41–43 đã nói về checked vs unchecked — đây là hệ quả thực tế của nó.)
// ✅ VÀ NÊN VIẾT khi muốn đặt tên cho một QUY TẮC hay thay đổi:
Map<String, ChinhSachGiamGia> chinhSach = Map.of(
"TET", (d, k) -> d.tong().multiply(new BigDecimal("0.20")),
"VIP", (d, k) -> k.hangThanhVien() == VANG ? d.tong().multiply(...) : ZERO,
"KHONG", (d, k) -> BigDecimal.ZERO);
// ⭐ Thêm chương trình khuyến mãi mới = thêm MỘT DÒNG, không sửa if-else nào.
// Đây là mẫu Strategy, viết bằng lambda thay vì bằng năm class riêng.ChinhSachGiamGiađọc là hiểu, cònBiFunction<DonHang,KhachHang,BigDecimal>thì đúng kiểu nhưng vô hồn.Điểm đau lớn nhất: toàn bộ
java.util.functionkhông khai ném checked exception — nên trong stream bạn phải bọc lại, hoặc tự định nghĩa interface.Một
Mapcác chính sách là mẫu Strategy viết bằng lambda: thêm khuyến mãi mới chỉ là thêm một dòng, không sửaif-elsenào.
Bài tập nhỏ
Viết bốn lambda cho bốn interface chính rồi gọi thẳng
apply,accept,get,test.Đo thời gian chạy một triệu lần với
Function<Integer,Integer>và vớiIntUnaryOperator.Ghép ba
Predicatebằngandvànegate, đặt tên có nghĩa cho từng cái.Thử gọi
Files.deletetrongforEachđể gặp lỗi checked exception, rồi tự viếtConsumerNemDuoc.Chuyển một khối
if-elsetính giảm giá thành mộtMapcác chính sách.
Kết lại
Bốn ý gói lại hôm nay: bốn interface phân biệt bằng hai câu hỏi — nhận gì vào, trả gì ra — và chúng tồn tại vì Java không có kiểu hàm; biến thể nguyên thủy tránh boxing, và trên vòng lặp triệu phần tử thì đó là khác biệt thật; chỉ dùng andThen để chuỗi đọc theo đúng thứ tự xảy ra, còn giá trị lớn nhất của Predicate là đặt được tên cho điều kiện; và tự viết interface khi cái tên mang ý nghĩa nghiệp vụ hoặc khi cần ném checked exception — điểm đau lớn nhất của lập trình hàm trong Java. Ngày 53 là method reference: bốn loại, và khi nào nó thật sự dễ đọc hơn lambda. Hẹn gặp lại!
Site Admin
Engineer and writer. Building things with TypeScript and distributed systems.
Bình luận (0)
Bạn cần đăng nhập bằng Google để bình luận.
Hãy là người bình luận đầu tiên.


