Java OO & Functional
28/38
Bài 28 / 38~12 phútStream API & LambdaLộ trình · chặng 06/21Miễn phí… lượt xem

Lambda và Functional Interface trong Java

Lambda expression, Single Abstract Method (SAM), 4 functional interface cốt lõi và nguyên tắc effectively final trong quản lý ấn phẩm thư viện.

TL;DR: Lambda Expression biến một hành vi logic thành một giá trị có thể truyền nhận như dữ liệu, thay thế các lớp nội danh cồng kềnh (Anonymous Inner Class). Trình biên dịch Java suy luận kiểu của Lambda thông qua Target Type—vị trí sử dụng phải là một Functional Interface sở hữu đúng một phương thức trừu tượng duy nhất (Single Abstract Method - SAM). Khi một Lambda truy cập biến cục bộ bên ngoài scope, biến đó bắt buộc phải là Effectively Final (không bao giờ gán lại) nhằm ngăn chặn xung đột dữ liệu giữa Stack và Heap.

1. Cú pháp Lambda Expression thay thế Anonymous Inner Class ra sao?

Trước Java 8, mỗi khi muốn truyền một hành vi xử lý (như tiêu chí lọc ấn phẩm quá hạn hay quy tắc so sánh giá sách), lập trình viên buộc phải tạo một Anonymous Inner Class với nhiều dòng boilerplate:

Java
// Pre-Java 8: Verbose Anonymous Inner Class
Collections.sort(books, new Comparator<CatalogBook>() {
    @Override
    public int compare(CatalogBook a, CatalogBook b) {
        return Integer.compare(a.getPublishYear(), b.getPublishYear());
    }
});

Cú pháp này buộc lập trình viên phải khai báo lại tên lớp, tên phương thức và kiểu dữ liệu lặp đi lặp lại. Từ Java 8, đoạn mã trên được rút gọn thành:

Java
// Java 8+: Compact Lambda Expression
Collections.sort(books, (a, b) -> Integer.compare(a.getPublishYear(), b.getPublishYear()));

Toàn bộ lớp vỏ bọc được lược bỏ, giữ lại đúng bản chất logic nghiệp vụ: nhận vào hai tham số (a, b) và trả về kết quả so sánh.

2. Functional Interface (SAM) định hình Target Type của Lambda như thế nào?

Bản thân một biểu thức Lambda (a, b) -> ... không có kiểu dữ liệu độc lập. Trình biên dịch xác định kiểu của nó dựa vào ngữ cảnh nơi nó được gán—gọi là Target Type. Target Type bắt buộc phải là một Functional Interface: giao diện có đúng một phương thức trừu tượng (Single Abstract Method - SAM).

flowchart TD
    A["Biểu thức Lambda: (item) -> item.isOverdue()"] --> B{"Kiểm tra Target Type"}
    B -->|"Predicate&lt;LibraryItem&gt;"| C["Khớp SAM: boolean test(T t)"]
    C --> D["Compile Thành Công: Sinh CallSite qua invokedynamic"]
    B -->|"Không phải SAM (>1 abstract method)"| E["Compile Error: Not a functional interface"]
Java
@FunctionalInterface
public interface ItemFilter {
    boolean accept(LibraryItem item); // Exactly one single abstract method

    // Default methods and static methods do not break SAM contract:
    default ItemFilter and(ItemFilter other) {
        return item -> this.accept(item) && other.accept(item);
    }
}

// Valid lambda assignment:
ItemFilter overdueFilter = item -> item.isOverdue();

[!NOTE] Các phương thức kế thừa từ java.lang.Object (như equals(), toString()) không được tính vào số lượng abstract method của SAM. Ví dụ: Comparator khai báo cả compare() lẫn equals(), nhưng vẫn là một Functional Interface chuẩn mực.

3. Bốn Functional Interface cốt lõi trong java.util.function là gì?

Gói java.util.function cung cấp hơn 40 giao diện chức năng chuẩn hóa, trong đó có 4 giao diện nền tảng:

Java
// 1. Function<T, R>: Transforms T into R
Function<LibraryItem, String> labelGenerator = item -> "Code: " + item.getBarcode();
String label = labelGenerator.apply(book);

// 2. Predicate<T>: Evaluates condition on T, returns boolean
Predicate<LibraryItem> isDamaged = item -> item.getConditionScore() < 50;
boolean needsRepair = isDamaged.test(book);

// 3. Consumer<T>: Consumes T, performs side-effects, returns void
Consumer<LibraryItem> auditLogger = item -> System.out.println("Audit: " + item.getTitle());
auditLogger.accept(book);

// 4. Supplier<T>: Takes no argument, lazily supplies T
Supplier<LibraryItem> defaultFallback = () -> new PrintedBook("DEFAULT-01", 0.5);
LibraryItem item = defaultFallback.get();
Functional InterfacePhương thức SAMÝ nghĩa nghiệp vụỨng dụng phổ biến trong Stream
Function<T, R>R apply(T t)Ánh xạ, biến đổi kiểu dữ liệustream.map(...)
Predicate<T>boolean test(T t)Bộ lọc, kiểm tra chân lýstream.filter(...)
Consumer<T>void accept(T t)Ghi log, in ấn, cập nhật trạng tháistream.forEach(...)
Supplier<T>T get()Sinh giá trị lười, cấp phát dự phòngoptional.orElseGet(...)

Tối ưu hóa với kiểu nguyên thủy (Primitive Specializations)

Để tránh lãng phí bộ nhớ và CPU do Autoboxing (chuyển đổi liên tục giữa int và Integer), hãy sử dụng các biến thể chuyên biệt: IntPredicate, LongFunction, DoubleConsumer, ToIntFunction.

4. Tại sao biến cục bộ bị capture bắt buộc phải là Effectively Final?

Lambda Expression có thể truy cập các biến cục bộ (Local Variables) ở scope bên ngoài. Tuy nhiên, các biến này bắt buộc phải là final hoặc Effectively Final (không bị gán lại sau khi khởi tạo):

Java
int thresholdDays = 30; // Stack local variable

Predicate<LoanRecord> isLate = record -> record.getDaysOverdue() > thresholdDays; // Valid

// If reassigned later:
// thresholdDays = 45; // COMPILE ERROR: Variable must be final or effectively final

Cơ chế Stack và Heap bên dưới

Biến cục bộ nằm trên Stack Frame của hàm. Khi hàm kết thúc, Stack Frame bị hủy và biến cục bộ biến mất. Trong khi đó, Lambda là một đối tượng nằm trên Heap và có thể được truyền sang một Thread khác để chạy bất đồng bộ vào thời điểm sau này.

Để giải quyết mâu thuẫn này, JVM thực hiện cơ chế Capture by Value: copy giá trị của biến từ Stack sang trường dữ liệu của đối tượng Lambda trên Heap.

Nếu Java cho phép biến cục bộ trên Stack thay đổi sau khi copy, giá trị trong Lambda sẽ bị lệch pha với giá trị trên Stack, gây mất nhất quán dữ liệu và sinh lỗi Race Condition trong môi trường đa luồng. Việc áp đặt effectively final là lựa chọn thiết kế để bảo đảm an toàn dữ liệu.

5. Từ khóa this trong Lambda khác biệt thế nào với Anonymous Inner Class?

Một điểm khác biệt ngữ nghĩa rất quan trọng giữa Lambda và lớp nội danh nằm ở từ khóa this:

Java
public class CatalogService {
    private final String serviceId = "CATALOG_V1";

    public void demoScope() {
        // 1. Anonymous Inner Class:
        Runnable r1 = new Runnable() {
            @Override
            public void run() {
                // 'this' refers to the anonymous Runnable instance!
                System.out.println("Inner: " + this.getClass().getName());
            }
        };

        // 2. Lambda Expression:
        Runnable r2 = () -> {
            // 'this' refers to the enclosing CatalogService instance!
            System.out.println("Lambda: " + this.serviceId);
        };
    }
}

Lambda không tạo ra một phạm vi thực thể (Instance Scope) riêng biệt. Từ khóa this bên trong Lambda được kế thừa trực tiếp từ phạm vi bao quanh nó (Lexical Scoping).

6. Đâu là những cạm bẫy phổ biến khi sử dụng Lambda?

❌ Cạm bẫy 1: Cố gắng mutate trạng thái bên ngoài bằng cách lách biến mảng

Java
// Anti-pattern: Using 1-element array to bypass effectively final
int[] counter = {0};
books.forEach(b -> counter[0]++); // Thread-unsafe code smell!

// Correct: Use Stream API count operation
long count = books.stream().count();

❌ Cạm bẫy 2: Dùng Function<T, Void> cho tác vụ Side-effect

Java
// Incorrect semantic:
Function<LibraryItem, Void> logger = item -> { System.out.println(item); return null; };

// Idiomatic:
Consumer<LibraryItem> logger = item -> System.out.println(item);

❌ Cạm bẫy 3: Viết Lambda quá dài với nhiều khối ngoặc nhọn Nếu một Lambda dài hơn 3 dòng hoặc chứa nhiều logic rẽ nhánh phức tạp, hãy tách nó thành một private method độc lập và sử dụng Method Reference (xem chi tiết ở Bài 02 — Method Reference).

7. Đọc thêm từ tài liệu Oracle và các liên kết liên quan

Tài liệu tham khảo chuyên sâu

8. Tóm tắt các điểm mấu chốt

  • Lambda Expression biểu diễn hành động dưới dạng giá trị, loại bỏ boilerplate của Anonymous Inner Class.
  • Target Type của Lambda bắt buộc phải là một Functional Interface sở hữu duy nhất một phương thức trừu tượng (SAM).
  • @FunctionalInterface bảo đảm hợp đồng thiết kế không bị phá vỡ bởi các abstract method bổ sung.
  • Bốn functional interface cốt lõi gồm: Function (biến đổi), Predicate (điều kiện), Consumer (hành động phụ), Supplier (sinh lười).
  • Biến cục bộ trong phạm vi bao ngoài bắt buộc phải là effectively final để bảo đảm tính toàn vẹn khi copy giữa Stack và Heap.

9. Câu hỏi tự kiểm tra

Tự kiểm tra
0/5 câu đã trả lời
  1. Q1
    Vì sao một interface có hai default method và một abstract method vẫn được xem là một Functional Interface hợp lệ?
  2. Q2
    Nguyên nhân gốc rễ ở tầng kiến trúc bộ nhớ JVM khiến biến cục bộ dùng trong Lambda phải là effectively final là gì?
  3. Q3
    Điểm khác biệt giữa từ khóa this bên trong Lambda Expression và this trong Anonymous Inner Class là gì?
  4. Q4
    Khi nào bạn nên sử dụng IntPredicate thay vì Predicate<Integer>?
  5. Q5
    Tại sao dòng lệnh sau đây báo lỗi biên dịch: var action = (x) -> x * 2;?

Bài tiếp theo: Method Reference và invokedynamic — bytecode đằng sau Lambda

Bài này có giúp bạn hiểu bản chất không?

Bài này đáng gửi cho bạn học cùng?

Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.

Lộ trình Junior Java Backend Developer

Chặng 6/21 · Lambda & Stream API — bài 2/12 của chặng

Thảo luận & góp ý

Hỏi đáp về bài này

Bài tiếp theo

Method Reference và invokedynamic: Cơ Chế Bytecode Bên Dưới Lambda