Lambda & functional interface: lọc đơn hàng mà không cần anonymous class
Mình muốn học Java 8.
Được. Học Java 8 để làm backend thì thứ tự nên là: Lambda → Functional Interface → Method Reference → Stream API → Optional. Lambda là nền, Stream ở bài 1.12 xây trên nó, Optional ở bài 1.13. Bài này dùng tiếp shop-backend: một service cần lọc danh sách Order theo nhiều điều kiện khác nhau.
1. Bài toán: mỗi điều kiện lọc là một method
public record Order(String id, String customerId, OrderStatus status, BigDecimal total) {}
class OrderReportService {
List<Order> paidOrders(List<Order> orders) {
List<Order> result = new ArrayList<>();
for (Order o : orders) if (o.status() == OrderStatus.PAID) result.add(o);
return result;
}
List<Order> bigOrders(List<Order> orders) { // copy y hệt, chỉ khác điều kiện
List<Order> result = new ArrayList<>();
for (Order o : orders) if (o.total().compareTo(BigDecimal.valueOf(1_000_000)) > 0) result.add(o);
return result;
}
}
2. Vấn đề
- Vòng lặp giống nhau, chỉ khác một dòng điều kiện. Thêm "đơn của khách VIP" là thêm một method nữa.
- Muốn truyền "điều kiện" vào method thì trước Java 8 phải dùng anonymous class: 6 dòng boilerplate để nói một biểu thức boolean.
List<Order> filter(List<Order> orders, OrderFilter f) { ... }
filter(orders, new OrderFilter() {
@Override
public boolean accept(Order o) {
return o.status() == OrderStatus.PAID;
}
});
3. Giải pháp: lambda + functional interface
- Functional interface: interface có đúng một abstract method (default/static không tính).
@FunctionalInterfacekhông bắt buộc, chỉ để compiler báo lỗi nếu ai thêm method thứ hai. - Lambda: cú pháp ngắn để cung cấp implementation cho method đó:
(params) -> expressionhoặc(params) -> { statements; }. Lambda không có kiểu riêng; kiểu của nó là target type mà compiler suy ra từ bên trái hoặc tham số. - Java 8 có sẵn trong
java.util.functionnên thường không cần tự viết interface.
4. Code từng bước
Bước 1: Một method lọc chung, nhận Predicate
static List<Order> filter(List<Order> orders, Predicate<Order> condition) {
List<Order> result = new ArrayList<>();
for (Order o : orders) if (condition.test(o)) result.add(o);
return result;
}
Bước 2: Truyền điều kiện bằng lambda
List<Order> paid = filter(orders, o -> o.status() == OrderStatus.PAID);
List<Order> big = filter(orders, o -> o.total().compareTo(BigDecimal.valueOf(1_000_000)) > 0);
// Predicate có default method để ghép điều kiện
Predicate<Order> isPaid = o -> o.status() == OrderStatus.PAID;
Predicate<Order> isBig = o -> o.total().compareTo(BigDecimal.valueOf(1_000_000)) > 0;
List<Order> paidAndBig = filter(orders, isPaid.and(isBig));
List<Order> unpaid = filter(orders, isPaid.negate());
Bước 3: Các functional interface còn lại trên cùng project
Function<Order, String> toCustomer = o -> o.customerId(); // T → R
Consumer<Order> audit = o -> log.info("Order {}", o.id()); // T → void
Supplier<String> newId = () -> UUID.randomUUID().toString(); // () → T
BiFunction<BigDecimal, BigDecimal, BigDecimal> add = (a, b) -> a.add(b); // (T, U) → R
BinaryOperator<BigDecimal> add2 = BigDecimal::add; // (T, T) → T
UnaryOperator<String> upper = String::toUpperCase; // T → T
Bước 4: Method reference: lambda chỉ gọi đúng một method có sẵn
o -> o.customerId() ≡ Order::customerId // instance method của tham số
s -> Integer.parseInt(s) ≡ Integer::parseInt // static method
o -> auditService.record(o) ≡ auditService::record // method của một object cụ thể
() -> new ArrayList<Order>() ≡ ArrayList::new // constructor
5. Cách hoạt động
filter(orders, o -> o.status() == PAID)
└──────┬──────────────┘
compiler nhìn tham số: Predicate<Order> (target type)
│ abstract method duy nhất: boolean test(Order)
▼
lambda = implementation của test()
│ bytecode: invokedynamic → LambdaMetafactory
▼ (không sinh file OrderService$1.class như anonymous class)
một object implement Predicate<Order>
│
filter() gọi condition.test(o) cho từng order
6. So sánh
| Interface | Method | Dạng | Ví dụ shop-backend |
|---|---|---|---|
Predicate<T> | test | T → boolean | đơn đã thanh toán? |
Function<T,R> | apply | T → R | Order → customerId, Entity → DTO |
Consumer<T> | accept | T → void | ghi log, gửi email |
Supplier<T> | get | () → T | sinh id, giá trị mặc định lazy |
BiFunction<T,U,R> | apply | (T, U) → R | cộng tiền, merge hai map |
Runnable / Callable<V> | run / call | () → void / V | task cho thread pool |
Comparator<T> | compare | (T, T) → int | sắp xếp đơn theo total |
Có bản primitive để tránh boxing: IntPredicate, ToLongFunction, IntSupplier...
7. Khi nào nên dùng / không nên
- Nên: truyền hành vi ngắn (điều kiện, mapping, callback, comparator), stream, cấu hình Spring kiểu
http.authorizeHttpRequests(auth -> ...). - Không nên: lambda dài 20 dòng. Tách ra method có tên rồi dùng method reference (
this::isEligibleForVoucher), dễ đọc và dễ test hơn. - Không nên: lambda ném checked exception lung tung;
Functionkhông khai báothrows, phải bọc lại.
Hai câu hỏi:
- Bạn nói lambda thường dùng với functional interface, vậy còn trường hợp nào khác không?
- Trả lời luôn câu bạn hỏi lúc nãy: lambda khác anonymous class thế nào?
Câu "thường dùng" là chưa chính xác, cần chỉnh lại: lambda bắt buộc phải có target type là một functional interface. Không có trường hợp lambda implement class hay interface có hai abstract method.
Predicate<Order> p = o -> o.total().signum() > 0; // ✅ target type = Predicate
var p = o -> o.total().signum() > 0; // ❌ không có target type
Object p = () -> {}; // ❌ Object không phải functional interface
abstract class Notifier { abstract void send(Order o); }
Notifier n = o -> {}; // ❌ lambda không extend class được
Lambda khác anonymous class thế nào? Nhìn thì giống viết tắt, nhưng semantics khác:
| Lambda | Anonymous class | |
|---|---|---|
| Target | Chỉ functional interface | Interface bất kỳ hoặc extend class |
this | Là object bao ngoài (không có scope riêng) | Là chính object anonymous |
| State | Không khai báo field | Có field, nhiều method |
| Bytecode | invokedynamic, tạo lúc runtime | Sinh file Outer$1.class |
| Shadow biến | Không được đặt tên tham số trùng biến local | Được (scope mới) |
public class OrderReportService {
public void demo() {
Runnable a = new Runnable() {
public void run() { System.out.println(this); } // OrderReportService$1@...
};
Runnable b = () -> System.out.println(this); // OrderReportService@...
a.run(); b.run();
}
}
OrderReportService object ◄── this trong lambda
└── anonymous Runnable ◄── this trong anonymous class
Vì vậy câu "lambda là syntactic sugar của anonymous class" không nên trả lời "có" trọn vẹn: cú pháp thay thế được trong nhiều trường hợp, nhưng this, scope và cách compile khác nhau.
Mình viết threshold = threshold.add(x) rồi dùng trong lambda thì bị lỗi "must be final or effectively final". Sao vậy?
Đúng, đó là luật effectively final: lambda (và anonymous class từ Java 8) chỉ được đọc biến local mà không bị gán lại sau khi khởi tạo, dù không ghi chữ final.
BigDecimal threshold = BigDecimal.valueOf(1_000_000);
filter(orders, o -> o.total().compareTo(threshold) > 0); // ✅ effectively final
BigDecimal threshold = BigDecimal.valueOf(1_000_000);
threshold = threshold.add(BigDecimal.TEN); // gán lại
filter(orders, o -> o.total().compareTo(threshold) > 0); // ❌ compile error
BigDecimal sum = BigDecimal.ZERO;
orders.forEach(o -> sum = sum.add(o.total())); // ❌ không cộng dồn kiểu này được
Lý do: lambda capture giá trị (copy) của biến local, và lambda có thể chạy sau khi method đã return hoặc ở thread khác. Nếu cho gán lại, bản copy và biến gốc lệch nhau, còn đa thread thì race condition. Field của object thì không bị luật này (vì đọc qua this), nhưng sửa field trong lambda chạy song song vẫn không an toàn.
Cách đúng để cộng doanh thu là không mutate gì cả: orders.stream().map(Order::total).reduce(BigDecimal.ZERO, BigDecimal::add), chính là bài 1.12.
Phỏng vấn hay hỏi "lambda là gì" hoặc "em biết gì về lambda". Trả lời phần lý thuyết thế nào?
Đừng lao vào code ngay. Đi theo: định nghĩa → mục đích → target type → ví dụ → lưu ý. (Câu "Java 8 có functional interface nào" đã có ở bài 1.2; ở đây đào sâu hơn.)
Câu 1: Lambda là gì? Em biết gì về lambda?
🇻🇳 Tiếng Việt:
Lambda là tính năng từ Java 8, cho phép viết ngắn gọn implementation của một functional interface, tức interface có đúng một abstract method, mà không cần anonymous class. Cú pháp là
(params) -> expression. Lambda không có kiểu riêng, compiler suy ra target type từ ngữ cảnh. Mục đích là truyền hành vi như dữ liệu, giúp code gọn, nhất là với Stream API. Ví dụ trong dự án em lọc đơn đã thanh toán bằngorders.stream().filter(o -> o.status() == PAID). Java có sẵnPredicate,Function,Consumer,Supplier. Lambda chỉ dùng được biến local effectively final.
🇬🇧 English:
A lambda, introduced in Java 8, is a concise way to implement a functional interface, an interface with exactly one abstract method, without writing an anonymous class. The syntax is
(params) -> expression. A lambda has no type of its own; the compiler infers its target type from the context. The point is to pass behavior as data, which keeps code short, especially with the Stream API. In my project I filter paid orders withorders.stream().filter(o -> o.status() == PAID). The JDK providesPredicate,Function,ConsumerandSupplier. A lambda can only capture effectively final local variables.
Câu 2: Lambda có phải syntactic sugar của anonymous class không?
🇻🇳 Tiếng Việt:
Không hoàn toàn. Về cú pháp nó thay được anonymous class khi target là functional interface, nhưng semantics khác: lambda không tạo scope
thisriêng,thislà object bao ngoài; lambda không có field và không extend class được. Về bytecode, lambda dùnginvokedynamicvà tạo implementation lúc runtime, không sinh file$1.class.
🇬🇧 English:
Not exactly. Syntactically it replaces an anonymous class when the target is a functional interface, but the semantics differ: a lambda has no
thisof its own,thisrefers to the enclosing instance; it cannot have fields or extend a class. At the bytecode level it usesinvokedynamicand the implementation is created at runtime instead of a separate$1.classfile.
Câu 3: Effectively final là gì, vì sao lambda cần nó?
🇻🇳 Tiếng Việt:
Là biến local không bị gán lại sau khi khởi tạo dù không ghi
final. Lambda capture bản copy giá trị và có thể chạy sau hoặc ở thread khác, nên Java cấm gán lại để tránh lệch giá trị và race condition. Muốn cộng dồn thì dùngreducehoặccollectthay vì mutate biến ngoài.
🇬🇧 English:
It's a local variable that is never reassigned after initialization, even without the
finalkeyword. A lambda captures a copy of the value and may run later or on another thread, so Java forbids reassignment to avoid stale values and race conditions. To accumulate, usereduceorcollectinstead of mutating an outer variable.
Câu 4: Method reference là gì?
🇻🇳 Tiếng Việt:
Là cách viết ngắn hơn của lambda khi lambda chỉ gọi đúng một method có sẵn, ví dụ
Order::customerIdthay choo -> o.customerId(). Có bốn dạng: static method, instance method của tham số, instance method của object cụ thể, và constructorArrayList::new.
🇬🇧 English:
It's a shorter form of a lambda that just calls one existing method, e.g.
Order::customerIdinstead ofo -> o.customerId(). There are four kinds: static method, instance method of the parameter, instance method of a specific object, and constructor references likeArrayList::new.
Cách nhớ ngắn gọn
- Lambda = implementation của functional interface (1 abstract method), không có kiểu riêng.
- Predicate test, Function apply, Consumer accept, Supplier get.
thistrong lambda = object bao ngoài; anonymous class cóthisriêng.- Biến local trong lambda phải effectively final.
- Lambda chỉ gọi 1 method có sẵn → method reference
::.
Tự kiểm tra
var f = x -> x * 2; có compile không?
Không. Lambda cần target type là functional interface; var không cho compiler biết interface nào.
Interface có 1 abstract method và 3 default method có phải functional interface không?
Có. Chỉ đếm abstract method; default, static và method public của Object không tính.
In this trong lambda bên trong method của OrderReportService ra gì?
Instance OrderReportService bao ngoài, vì lambda không tạo scope this riêng.
Muốn tạo id mới chỉ khi cần (lazy) thì dùng interface nào?
Supplier<String>: () -> UUID.randomUUID().toString(), chỉ chạy khi gọi get().
Viết lại o -> o.total() bằng method reference.
Order::total.