Welcome to MikeDaDev's Blog – notes on Java, backend & system design.

Explore
MikeDaDev's Blog
in java

functional interfaces & lambda trong java

Tung Bui Thanh (TungDaDev)10 mins read

Functional interfaces lambda expressions.webp

Trước Java 8, Java hay bị chê là dài dòng (verbose) và cổ hủ. Muốn truyền một hành vi đơn giản vào method (ví dụ một Comparator để so sánh hay một Runnable để chạy), bạn phải viết Anonymous Inner Class kèm cả đống boilerplate code.

Functional Interfaces và Lambda Expressions trong Java 8 không chỉ là syntactic sugar cho gọn. Nó thay đổi khá nhiều thứ bên trong JVM:

  • Không sinh thêm file .class cho mỗi lambda, thay vào đó dùng lệnh bytecode invokedynamic (có từ Java 7, qua JSR 292) và LambdaMetafactory.
  • Mở đường cho lập trình hàm (Functional Programming), coi hàm như một giá trị bình thường (First-Class Citizens).
  • Tiết kiệm bộ nhớ nhờ Non-Capturing Lambda được cache lại như singleton.

Phần đầu nhìn xuống bytecode xem JVM thật sự làm gì, phần sau dùng lambda dựng thử một Policy Engine.


# dưới tầng bytecode: vì sao lambda không phải là anonymous inner class?

Nhiều bạn nghĩ Runnable r = () -> {} chỉ là cách viết tắt của Runnable r = new Runnable() { public void run() {} }.

Với JVM thì hai thứ này khác nhau khá xa:

# so sánh chi phí thực tế giữa hai cách

Tiêu chí kỹ thuậtAnonymous Inner ClassLambda Expression
Tạo file trên đĩaSinh riêng một file Class$1.class.Không sinh file .class nào trên đĩa.
Cách nạp classClassLoader phải đọc đĩa, parse bytecode lúc startup.Sinh class ngay trong bộ nhớ lúc lambda chạy lần đầu (Java 8 dùng Unsafe.defineAnonymousClass, từ JDK 15 dùng hidden class qua Lookup.defineHiddenClass).
Cấp phát bộ nhớ Heapnew Class$1() mỗi lần chạy (thêm rác cho GC).Non-capturing: cấp phát đúng 1 lần (singleton).
Capturing: mỗi lần chạy biểu thức lambda là một object nhỏ chứa các giá trị đã capture.
Con trỏ thisTrỏ tới chính instance của inner class. Trong method instance, nó ngầm giữ tham chiếu tới object bao ngoài (this$0); javac từ JDK 18 bỏ field này nếu không dùng tới.this trỏ tới object bao bên ngoài (Lexical Scoping). Lambda chỉ giữ this khi thân lambda dùng tới field/method instance.

Hai lưu ý cho công bằng. Chuyện non-capturing lambda dùng lại một instance là cách cài đặt hiện tại của OpenJDK, đặc tả Java không hứa điều đó, nên đừng viết code dựa vào == giữa hai lambda. Và lambda không miễn phí lúc khởi động: lần đầu một lệnh invokedynamic chạy, JVM phải gọi bootstrap method và sinh class, tốn hơn nạp một inner class có sẵn. Lambda đầu tiên trong app còn phải khởi tạo cả hạ tầng java.lang.invoke; JDK giảm bớt chi phí này bằng cách lưu sẵn một phần trong CDS archive.


# mổ xẻ bytecode invokedynamic & LambdaMetafactory

Xem đoạn code này:

public class LambdaDemo {
    public void execute() {
        Runnable r = () -> System.out.println("Executing task...");
        r.run();
    }
}

Chạy javap -c -p LambdaDemo.class sẽ thấy compiler không hề sinh class ẩn danh nào, mà chèn vào một lệnh bytecode invokedynamic:

0: invokedynamic #7, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable;
5: astore_1
6: aload_1
7: invokeinterface #11, 1 // InterfaceMethod java/lang/Runnable.run:()V
12: return

Còn phần thân của lambda thì javac tách ra thành một method private static bên trong class:

private static void lambda$execute$0();
    0: getstatic #15 // Field java/lang/System.out:Ljava/io/PrintStream;
    3: ldc #21       // String Executing task...
    5: invokevirtual #23 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    8: return

# bản chất của biến effectively final & memory closure

Câu phỏng vấn Java kinh điển: Tại sao biến local bên ngoài mà dùng trong lambda thì bắt buộc phải là final hoặc effectively final?

public void testScope() {
    int port = 8080;
    Runnable r = () -> System.out.println("Port: " + port); // OK
    port = 9090; // 🔴 BIÊN DỊCH LỖI: Variable used in lambda should be final or effectively final!
}

# gốc rễ nằm ở stack và heap

  1. Vòng đời khác nhau: biến local port nằm trên Stack của thread hiện tại. Hàm testScope() chạy xong là stack frame này bị giải phóng. Trong khi đó object lambda nằm trên Heap và có thể được đưa sang thread khác, hàng giờ sau mới chạy.
  2. Capture by Value: để lambda chạy được khi hàm cha đã xong, JVM phải copy giá trị của port vào một field bên trong object lambda trên Heap.
  3. Tránh hiểu lầm: nếu Java cho sửa port, dev sẽ tưởng sửa biến trên Stack thì giá trị trong lambda cũng đổi theo, hoặc ngược lại. Java không có con trỏ thật tới biến local trên Stack (như con trỏ trong C++), nên nó bắt biến phải không đổi (immutable), để bản sao trên Heap và biến gốc trên Stack luôn khớp nhau.

# bốn interface nền tảng trong gói java.util.function

Java 8 chuẩn hoá lập trình hàm qua 4 Functional Interface cơ bản:

Functional InterfaceChữ ký hàm (SAM)Hàm kết hợp (Default Methods)Dùng trong dự án thật
Predicate<T>boolean test(T t)and(), or(), negate(), static not() (Java 11)Lọc dữ liệu, validate rule, kiểm soát truy cập.
Function<T, R>R apply(T t)andThen(), compose(), static identity()Map DTO, pipeline biến đổi dữ liệu nghiệp vụ.
Consumer<T>void accept(T t)andThen()Ghi log, audit trail, publish message ra Kafka.
Supplier<T>T get()(Không có)Khởi tạo lazy (Optional.orElseGet), sinh token ngẫu nhiên.

TIP

Cẩn thận autoboxing: làm việc với kiểu nguyên thuỷ thì nên dùng các biến thể riêng như IntPredicate, LongFunction<R>, DoubleConsumer, tránh tốn công boxing/unboxing làm chậm chương trình.


# case study: dựng declarative policy engine

Trong một ngân hàng số, duyệt hồ sơ vay tín chấp (Loan Underwriting) phụ thuộc vào cả chục rule rủi ro, và rule thì đổi liên tục:

  • Tuổi từ 22 đến 60.
  • Thu nhập hàng tháng từ 15,000,000 VND trở lên.
  • Điểm tín dụng CIC từ 650 trở lên.
  • Không có nợ xấu từ nhóm 3 trở lên.

Thay vì viết một mớ if-else lồng nhau, mình kết hợp Higher-Order Functions với Predicate Chaining để có một Policy Engine gọn gàng:

package com.company.risk.engine;
 
import java.math.BigDecimal;
import java.util.Objects;
import java.util.function.Predicate;
 
// File LoanApplication.java
public record LoanApplication(
    String applicantId,
    int age,
    BigDecimal monthlyIncome,
    int creditScore,
    boolean hasBadDebt
) {}
 
// File UnderwritingPolicies.java (Java chỉ cho một public top-level type mỗi file)
public final class UnderwritingPolicies {
 
    private UnderwritingPolicies() {}
 
    // Các atomic predicate độc lập, dễ dàng Unit Test riêng biệt
    public static final Predicate<LoanApplication> IS_LEGAL_AGE =
        app -> app.age() >= 22 && app.age() <= 60;
 
    public static final Predicate<LoanApplication> HAS_SUFFICIENT_INCOME =
        app -> app.monthlyIncome().compareTo(BigDecimal.valueOf(15_000_000)) >= 0;
 
    public static final Predicate<LoanApplication> HAS_PRIME_CREDIT =
        app -> app.creditScore() >= 650;
 
    public static final Predicate<LoanApplication> NO_DELINQUENCY =
        app -> !app.hasBadDebt();
 
    // Kết hợp các policy thành một quy tắc tổng thể thông qua Higher-Order Composition
    public static Predicate<LoanApplication> standardUnsecuredLoanPolicy() {
        return IS_LEGAL_AGE
            .and(HAS_SUFFICIENT_INCOME)
            .and(HAS_PRIME_CREDIT)
            .and(NO_DELINQUENCY);
    }
}

# dùng trong service nghiệp vụ

@Service
public class LoanProcessingService {
 
    public UnderwritingResult evaluateApplication(LoanApplication application) {
        Predicate<LoanApplication> policy = UnderwritingPolicies.standardUnsecuredLoanPolicy();
 
        if (policy.test(application)) {
            return UnderwritingResult.approved("Hồ sơ đủ điều kiện phê duyệt tự động");
        }
 
        // Dễ dàng tìm ra chính xác rule nào bị vi phạm để thông báo cho khách hàng
        return UnderwritingResult.rejected(determineRejectionReason(application));
    }
 
    private String determineRejectionReason(LoanApplication app) {
        if (!UnderwritingPolicies.IS_LEGAL_AGE.test(app)) return "Độ tuổi không nằm trong khung chính sách";
        if (!UnderwritingPolicies.HAS_SUFFICIENT_INCOME.test(app)) return "Thu nhập chưa đạt mức tối thiểu";
        if (!UnderwritingPolicies.HAS_PRIME_CREDIT.test(app)) return "Điểm tín dụng CIC không đạt chuẩn";
        if (!UnderwritingPolicies.NO_DELINQUENCY.test(app)) return "Khách hàng có lịch sử nợ quá hạn";
        return "Vi phạm chính sách nội bộ";
    }
}

# tổng kết

Functional Interface và Lambda trong Java giúp được nhiều hơn là bớt vài dòng code rườm rà:

  1. Nhẹ hơn inner class: invokedynamic và LambdaMetafactory sinh class lúc chạy, không thêm file .class, non-capturing lambda không cấp phát lại mỗi lần. Đổi lại lần gọi đầu tốn chi phí bootstrap.
  2. Ngữ nghĩa rõ ràng: quy tắc effectively final bảo đảm bản sao trong lambda và biến gốc luôn cùng giá trị, khỏi phải đoán lambda đang thấy giá trị nào.
  3. Cách viết khai báo (declarative): tách luồng xử lý khỏi điều kiện nghiệp vụ bằng hàm bậc cao (Higher-Order Functions). Mỗi rule là một Predicate test riêng được, thêm hay bỏ rule chỉ là sửa chỗ ghép.

Chỉ là những ghi chép cá nhân với hy vọng mang lại chút giá trị. Nếu thấy hữu ích, đừng ngại chia sẻ cho bạn bè & đồng nghiệp nhé!

Happy coding 😎 👍🏻 🚀 🔥.