Lambda và Functional Interface
Xem thửTình huống mở đầu
Màn hình bếp quán Cà Phê Gió đã sắp được đơn theo ưu tiên. Giờ quản lý nhờ thêm ba việc: đếm đơn khẩn, đổi tên mỗi đơn thành chữ hoa để in lên màn hình, và xoá đơn không khẩn khi giờ cao điểm. Cả ba việc đều là "đi qua danh sách và làm một chuyện nhỏ khác nhau ở mỗi đơn". Bạn có thể viết ba vòng for, nhưng ba vòng gần giống nhau, chỉ khác một điều kiện hoặc một dòng ở giữa.
Điều bạn thật sự muốn là đưa phần khác nhau đó vào phương thức như một giá trị: "đây là điều kiện, hãy dùng nó". Trong bài trước bạn đã dùng o -> o.priority như khuôn mẫu mà chưa biết nó là gì. Vậy đoạn code đó là gì, nó chạy lúc nào, và vì sao Java lại hay báo lỗi must be final or effectively final?
Ôn nhanh bài trước: Comparator.comparingInt(o -> o.priority).thenComparing(...) ghép hai tiêu chí. Tiêu chí thứ hai chỉ được hỏi khi nào?
Đáp án: khi tiêu chí thứ nhất trả về 0, nghĩa là hai đơn hoà.
Mục tiêu bài học
Sau bài này, bạn có thể:
- Viết được lambda (không tham số, một tham số, nhiều tham số, thân khối) và gán nó cho
Predicate,Function,ConsumerhoặcSupplier. - Giải thích được functional interface là gì (đúng một phương thức trừu tượng) và thân của lambda chạy vào lúc nào.
- Dùng lambda với
removeIf,forEachvàsorttrên danh sách đơn của màn hình bếp. - Giải thích được vì sao biến local mà lambda dùng phải final hoặc effectively final.
Bài này đo được bằng năm câu trong phần Tự kiểm tra, mỗi câu ghi rõ nó kiểm tra mục tiêu nào.
Hãy xem video dưới đây để có cái nhìn đầu tiên, sau đó mình đi chậm hơn từng bước ở các phần sau.
Bức tranh lớn (Big Picture)
Bài này mở chương Java 8. Ý chính của cả chương là: code cũng có thể là một giá trị đưa vào phương thức.
Cách đọc hình: đi từ trái sang phải. Ô trắng bên trái là lambda, một đoạn code ngắn. Nó "fills" (lấp vào) ô giữa là functional interface, một chỗ trống chỉ có một phương thức. Ba ô vàng bên phải là các phương thức của thư viện: removeIf, forEach, sort, mỗi phương thức hỏi chỗ trống đó khi cần. Mỗi ô đều có chữ, màu chỉ là tín hiệu phụ.
Ẩn dụ xuyên suốt: lambda là một tờ phiếu hướng dẫn ngắn bạn đưa cho nhân viên bếp ("đơn nào ưu tiên 1 thì in lên màn hình"). Functional interface là mẫu phiếu có đúng một ô để điền việc. Nhân viên (như removeIf) cầm phiếu và làm theo cho từng đơn. Chỗ ẩn dụ không còn đúng: phiếu giấy chỉ đọc được thì tờ phiếu này còn nhớ được cả giá trị của các biến xung quanh nó, và đó là chuyện của khái niệm cuối bài.
Dự án xuyên suốt vẫn là màn hình bếp quán Cà Phê Gió, giờ mỗi đơn có tên đồ uống và độ ưu tiên.
Khái niệm cốt lõi
Bài này có bốn khái niệm, mỗi khái niệm đi kèm một hình.
Khái niệm 1: functional interface, mẫu phiếu có đúng một ô
Functional interface là interface có đúng một phương thức trừu tượng (không có thân). Phương thức default và static không tính. Nhờ chỉ có một ô, Java biết đoạn code bạn đưa vào là để lấp ô nào. Bạn có thể đánh dấu @FunctionalInterface để trình biên dịch giúp kiểm tra điều này.
Cách đọc hình: ba hàng, mỗi hàng đi từ trái sang phải. Hàng trên: Predicate có đúng một phương thức trừu tượng test nên nhận lambda. Hàng giữa màu đỏ: interface có hai phương thức trừu tượng nên báo lỗi "is not a functional interface". Hàng dưới: Comparator có một phương thức trừu tượng compare cùng nhiều phương thức default, vẫn nhận lambda. Dòng ghi chú cuối hình nhắc rằng chỉ phương thức trừu tượng mới được tính.
Khái niệm 2: lambda, cách viết gọn một phương thức
Lambda là một đoạn code ngắn viết thẳng tại chỗ để cài đặt phương thức duy nhất của functional interface. Dạng chung: danh sách tham số, dấu ->, rồi thân. Một biểu thức thì không cần return, nhiều lệnh thì dùng ngoặc nhọn và return.
Cách đọc hình: bốn hàng, mỗi hàng nối dạng lambda bên trái với interface nó thường lấp bên phải. Không tham số thì viết cặp ngoặc rỗng. Một tham số thì viết tên, không cần ngoặc. Hai tham số thì có ngoặc và dấu phẩy. Thân khối thì dùng ngoặc nhọn và return. Dòng ghi chú cuối hình nhắc kiểu của tham số thường được bỏ vì Java đọc từ interface.
Điều dễ nhầm nhất ở khái niệm này là thời điểm chạy: thân lambda không chạy ở dòng tạo ra nó. Phần Thử phá nó đi bên dưới có hình đặt mô hình sai và mô hình đúng cạnh nhau.
Concept Check 1. Dòng sau chạy trong main. Màn hình in gì ngay sau dòng này?
Predicate<Order> urgent = o -> { System.out.println("run"); return o.priority == 1; };
- A. In
runmột lần, vì dòng khai báo chạy thân lambda để kiểm tra nó. - B. In
runbốn lần, vì danh sách orders có bốn đơn để duyệt qua. - C. Không in gì cả, vì thân lambda chỉ chạy khi có ai gọi
urgent.test(...). - D. Lỗi biên dịch, vì lambda không được phép chứa lệnh in ra màn hình.
Đáp án: C. Khai báo chỉ tạo lambda và cất vào biến urgent. A là mô hình sai điển hình. B nhầm vì không có vòng lặp nào được gọi ở dòng này. D sai vì lambda dùng thân khối có thể chứa bao nhiêu lệnh tuỳ ý, miễn là có return khi interface trả giá trị.
Khái niệm 3: bốn functional interface hay dùng nhất
Java có sẵn bộ functional interface trong gói java.util.function. Bốn cái bạn gặp mỗi ngày: Predicate (nhận một giá trị, trả boolean, phương thức test), Function (nhận một giá trị, trả một giá trị khác, phương thức apply), Consumer (nhận một giá trị, không trả gì, phương thức accept), Supplier (không nhận gì, trả một giá trị, phương thức get). Trong bếp: Predicate trả lời "đơn này có khẩn không", Function đổi đơn thành nhãn in, Consumer in nhãn lên màn hình, Supplier tạo một đơn mặc định khi cần.
Cách đọc hình: bốn hàng, mỗi hàng nối interface bên trái với một cách dùng điển hình bên phải. Predicate nhận một đơn và trả đúng hoặc sai, dùng cho removeIf. Function nhận một đơn và trả nhãn chữ. Consumer nhận chữ và không trả gì, dùng cho forEach. Supplier không nhận gì và trả một đơn. Dòng ghi chú cuối nhắc cách nhớ theo hình dạng: hỏi có hoặc không, đổi, làm, tạo.
Khái niệm 4: lambda dùng biến local, và vì sao biến phải không đổi
Lambda có thể dùng biến local của phương thức xung quanh nó. Khi đó lambda giữ một bản sao giá trị của biến lúc nó được tạo. Vì thế biến phải là effectively final: chỉ được gán một lần, và sau đó không đổi (không cần gõ chữ final). Nếu biến đổi, bản sao trong lambda và biến gốc sẽ khác nhau, một cái tên mà hai giá trị, nên Java từ chối biên dịch. Đối tượng được bắt giữ thì khác: bạn không gán lại biến trỏ tới nó, chỉ sửa nội dung bên trong, nên vẫn được.
int limit = 6;
Predicate<Integer> big = n -> n > limit; // lambda giữ một bản sao của limit
nums.removeIf(big); // nums là 5, 8, 3, 10 nên còn lại 5 và 3
StringBuilder log = new StringBuilder();
nums.forEach(n -> log.append(n).append(';')); // sửa nội dung đối tượng thì được
| Dòng code | Máy làm gì | Vì sao quan trọng |
|---|---|---|
int limit = 6; | Tạo biến local và gán một lần | Gán đúng một lần là điều kiện để biến thành effectively final |
n -> n > limit | Tạo lambda giữ bản sao giá trị 6 | Về sau limit không được đổi, vì bản sao sẽ không đổi theo |
nums.removeIf(big) | Gọi big cho từng số, xoá số cho kết quả true | 8 và 10 lớn hơn 6 nên bị xoá |
log.append(n) | Thêm vào đối tượng StringBuilder đã được bắt giữ | Biến log vẫn trỏ tới cùng một đối tượng, chỉ nội dung đổi |
Kết quả của chương trình Capture [Đã chạy, JDK 17.0.15]:
[5, 3]
5;3;
Cách đọc hình: ô trắng bên trái là biến limit. Nhánh trên đi tới ô lục "Lambda keeps a copy" rồi ô "Object still editable": đối tượng được bắt giữ vẫn sửa được nội dung. Nhánh dưới nét đứt đi tới ô đỏ "limit changed later" rồi tới "Compile error". Dòng ghi chú cuối hình giải thích vì sao: một cái tên mà hai giá trị sẽ gây nhầm lẫn nên Java từ chối.
Hãy xem từng bước vì sao Java cấm đổi limit:
Đang tải Animation…
Bản chữ tương đương của animation:
| Bước | Dòng | Giá trị của limit | Giá trị lambda đang giữ |
|---|---|---|---|
| 1 | int limit = 6; | 6 | chưa có lambda |
| 2 | tạo big | 6 | bản sao 6 |
| 3 | nums.removeIf(big) | 6 | 6, kết quả [5, 3] |
| 4 | giả sử có limit = 7 | 7 | vẫn 6, hai giá trị khác nhau |
| 5 | biên dịch | không biên dịch được | lỗi must be final or effectively final |
Ví dụ đầu tiên (Worked Example) và "Xem từng bước"
Đây là chương trình đầu tiên của dự án bài này: màn hình bếp dùng bốn lambda của bốn functional interface đã học ở trên.
BoardLambda.java
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
public class BoardLambda {
static class Order {
final String drink;
final int priority;
Order(String drink, int priority) {
this.drink = drink;
this.priority = priority;
}
@Override
public String toString() {
return drink + ":" + priority;
}
}
public static void main(String[] args) {
List<Order> orders = new ArrayList<>(List.of(
new Order("Latte", 2), new Order("Mocha", 1), new Order("Tea", 3), new Order("Espresso", 1)));
// Bốn lambda này chỉ được tạo ra, thân của chúng chưa chạy
Predicate<Order> urgent = o -> o.priority == 1;
Function<Order, String> label = o -> o.drink.toUpperCase();
Consumer<String> show = s -> System.out.println("screen: " + s);
Supplier<Order> fallback = () -> new Order("Water", 3);
for (Order o : orders) {
if (urgent.test(o)) {
show.accept(label.apply(o)); // thân lambda chạy ở đây, mỗi lần gọi một lần
}
}
orders.removeIf(urgent.negate()); // negate đảo điều kiện: xoá đơn không khẩn
System.out.println(orders + " " + fallback.get());
}
}
Predict: hãy dừng lại, lấy giấy nháp và đoán: màn hình in những dòng nào, và dòng cuối cùng in ra gì? Đoán xong rồi hãy đọc tiếp.
Kết quả mong đợi [Đã chạy, JDK 17.0.15]:
screen: MOCHA
screen: ESPRESSO
[Mocha:1, Espresso:1] Water:3
Đọc code từng dòng:
| Dòng code | Máy làm gì | Vì sao viết như vậy |
|---|---|---|
o -> o.priority == 1 gán cho Predicate<Order> | Tạo lambda trả true khi đơn có ưu tiên 1 | Predicate có một phương thức test trả boolean, đúng điều kiện cần |
o -> o.drink.toUpperCase() gán cho Function<Order, String> | Tạo lambda đổi đơn thành tên đồ uống chữ hoa | Function nhận Order và trả String, hai kiểu trong ngoặc nhọn nói điều đó |
s -> System.out.println("screen: " + s) gán cho Consumer<String> | Tạo lambda in một chuỗi lên màn hình | Consumer chỉ làm một việc và không trả gì |
() -> new Order("Water", 3) gán cho Supplier<Order> | Tạo lambda sinh một đơn mới mỗi lần gọi | Supplier không nhận tham số nên dùng cặp ngoặc rỗng |
urgent.test(o) | Chạy thân của lambda urgent cho đơn o | Đây là lúc thân chạy, không phải lúc khai báo |
show.accept(label.apply(o)) | Đổi đơn thành nhãn rồi in nhãn | Hai lambda nối nhau: đầu ra của Function là đầu vào của Consumer |
orders.removeIf(urgent.negate()) | Xoá mọi đơn mà urgent trả false | negate() tạo điều kiện đảo ngược mà không viết lại lambda |
Bây giờ xem từng bước. Mỗi bước nêu dòng code đang chạy và số lần thân lambda đã chạy:
Đang tải Animation…
Bảng trace (bản chữ tương đương của animation, [Suy luận] bằng cách đi từng dòng và khớp với kết quả chạy thật):
| Dòng chạy | Thân lambda nào chạy | Tổng số lần chạy | Màn hình |
|---|---|---|---|
| bốn dòng khai báo lambda | không có | 0 | trống |
| vòng for, Latte:2 | urgent (false) | 1 | trống |
| vòng for, Mocha:1 | urgent, label, show | 4 | screen: MOCHA |
| vòng for, Tea:3 | urgent (false) | 5 | giữ nguyên |
| vòng for, Espresso:1 | urgent, label, show | 8 | thêm screen: ESPRESSO |
removeIf(urgent.negate()) | urgent bốn lần | 12 | giữ nguyên |
fallback.get() | fallback | 13 | in [Mocha:1, Espresso:1] Water:3 |
Bạn vừa làm được: phân biệt lúc lambda được tạo với lúc thân của nó chạy, và đếm đúng số lần chạy.
Concept Check xen kẽ
Chọn đáp án của bạn trước, rồi đọc phần đáp án ngay bên dưới.
Concept Check 2. Đoạn sau có biên dịch được không?
int limit = 6;
nums.removeIf(n -> n > limit);
limit = 7;
- A. Không. Lỗi báo ở dòng lambda, vì
limitbị gán lại sau đó nên không còn effectively final. - B. Có. Lambda dùng 6 vì lúc đó
limitlà 6, còn dòng cuối chỉ đổi biến sang 7. - C. Có. Lambda dùng 7 vì nó luôn nhìn thấy giá trị mới nhất của biến
limit. - D. Có, nhưng ném ngoại lệ lúc chạy ở dòng cuối vì lambda đang giữ biến
limit.
Đáp án: A. Chỉ cần có một lần gán lại ở bất cứ đâu, biến đã không còn effectively final, và lỗi được báo tại chỗ lambda dùng nó. B và C nhầm rằng lambda theo dõi biến: nó giữ bản sao, và Java chặn tình huống hai giá trị khác nhau thay vì chọn một. D sai vì lỗi này xảy ra lúc biên dịch, không phải lúc chạy.
Concept Check 3. Interface nào dùng được với lambda?
- A. Interface có hai phương thức trừu tượng
open()vàclose(). - B. Interface có một phương thức trừu tượng
price(), thêm một phương thức default và một phương thức static. - C. Một lớp abstract có đúng một phương thức trừu tượng.
- D. Interface rỗng, không có phương thức nào.
Đáp án: B. Phương thức default và static có thân sẵn nên không tính, chỉ còn một ô trừu tượng cho lambda lấp. A có hai ô nên Java không biết lambda lấp ô nào. C sai vì lambda chỉ dùng cho interface, không dùng cho lớp abstract. D không có ô nào để lấp.
Làm cùng mình (Guided Practice)
Bạn sẽ dựng dần class Screen qua năm bước nhỏ. Hỗ trợ giảm dần: bước 1 gần như làm sẵn, bước 5 chỉ còn đề. Gõ code vào file Screen.java rồi chạy bằng java Screen.java.
Bước 1: một Predicate.
import java.util.*;
import java.util.function.*;
public class Screen {
public static void main(String[] args) {
// TODO 1: viết lambda trả true khi chuỗi dài hơn 4 ký tự
Predicate<String> isLong = null;
System.out.println("step 1: " + isLong.test("Tea") + " " + isLong.test("Mocha"));
}
}
Chạy xong bạn sẽ thấy: step 1: false true.
Bước 2: một Function. Thêm dưới dòng in của bước 1:
// TODO 2: viết lambda trả độ dài của chuỗi
Function<String, Integer> len = null;
System.out.println("step 2: " + len.apply("Latte"));
Chạy xong bạn sẽ thấy: step 2: 5. Gợi ý: String::length là cách viết gọn của cùng việc này.
Bước 3: một Consumer đi qua danh sách.
List<String> names = new ArrayList<>(List.of("Latte", "Tea", "Mocha"));
StringBuilder initials = new StringBuilder();
// TODO 3: dùng forEach với lambda để nối chữ cái đầu của mỗi tên vào initials
System.out.println("step 3: " + initials);
Chạy xong bạn sẽ thấy: step 3: LTM.
Vẽ ra giấy: vẽ initials sau từng lần lambda chạy. Đối chiếu: L, rồi LT, rồi LTM. initials không bị gán lại, chỉ được nối thêm, nên lambda dùng được nó.
Bước 4: dùng biến local trong lambda. Bạn tự quyết cách viết.
int limit = 2;
List<Integer> nums = new ArrayList<>(List.of(1, 2, 3, 4));
// TODO 4: xoá mọi số lớn hơn limit bằng removeIf
System.out.println("step 4: " + nums);
Chạy xong bạn sẽ thấy: step 4: [1, 2]. Gợi ý: lambda dùng limit được vì limit chỉ được gán một lần.
Bước 5: sắp xếp bằng lambda.
// TODO 5: sắp names theo độ dài giảm dần bằng lambda hai tham số
System.out.println("step 5: " + names);
Chạy xong bạn sẽ thấy: step 5: [Latte, Mocha, Tea]. Gợi ý: hai tham số a, b, và đảo vai hai độ dài khi so sánh. Lời giải đầy đủ nằm ở cuối bài.
Bạn vừa làm được: tự viết lambda cho Predicate, Function, Consumer, dùng biến local trong lambda, và sắp xếp bằng lambda. Toàn bộ mã nguồn của bài cũng nằm trong thư mục 13-java8/01-lambda của kho GitHub, nhưng bạn không cần mở nó để học.
Thử phá nó đi (Lỗi thường gặp)
Bốn lỗi dưới đây là những lỗi người mới gặp nhiều nhất với lambda. Mỗi dòng đã được chạy thật [Đã chạy, JDK 17.0.15; thông báo lỗi có thể khác chữ ở phiên bản Java khác].
| Code lỗi | Thông báo lỗi | Nguyên nhân bằng lời thường | Cách sửa |
|---|---|---|---|
int limit = 2; Predicate<Integer> p = n -> n > limit; limit = 3; | local variables referenced from a lambda expression must be final or effectively final | Biến bị gán lại sau khi lambda dùng nó | Bỏ dòng gán lại, hoặc dùng biến mới cho giá trị mới |
int total = 0; orders.forEach(o -> total += o.priority); | Cùng thông báo như trên | += gán lại biến total ngay trong lambda | Dùng vòng for thường để cộng, hoặc bài sau dùng Stream |
Object o = () -> System.out.println("x"); | incompatible types: Object is not a functional interface | Object không có phương thức trừu tượng nào để lambda lấp | Gán cho một functional interface, như Runnable |
Function<Integer, Integer> f = (x, y) -> x + y; | incompatible types: incompatible parameter types in lambda expression | Function chỉ nhận một tham số mà lambda khai báo hai | Dùng BiFunction, hoặc viết lambda một tham số |
Lỗi thứ nhất và thứ hai cùng một gốc: lambda không được làm đổi biến local của phương thức bao quanh. Lỗi thứ ba và thứ tư cùng một gốc: lambda phải khớp với phương thức duy nhất của interface mà nó lấp (số tham số, kiểu trả về).
Hiểu lầm lớn nhất không có thông báo lỗi: tưởng rằng thân lambda chạy ngay khi dòng tạo ra nó được thực hiện. Hình dưới đây đặt mô hình sai và mô hình đúng cạnh nhau:
Cách đọc hình: hàng trên màu đỏ là mô hình sai: tin rằng dòng tạo lambda đã chạy thân của nó. Hàng dưới màu lục là mô hình đúng: tạo lambda chỉ cất hành vi lại, và không có gì xảy ra cho tới khi test được gọi. Dòng ghi chú cuối nói: lambda là một kế hoạch chứ không phải một hành động.
Bạn vừa làm được: đọc bốn thông báo lỗi phổ biến của lambda và nói được nguyên nhân bằng lời thường.
Tự tay làm (Bài tập)
Làm lần lượt từng mức. Mỗi bài có đề, dữ liệu vào, kết quả mong đợi, tiêu chí nghiệm thu và gợi ý. Lời giải nằm riêng ở cuối bài, hãy thử trước rồi mới so.
Mức Cơ bản
Bài 1. Điền bảng trace. Dữ liệu vào là đoạn code của khái niệm 4 với limit bằng 6 và nums là [5, 8, 3, 10]. Điền bảng gồm bốn dòng, mỗi dòng một số n: giá trị big.test(n) và số đó còn hay bị xoá. Kết quả mong đợi: danh sách cuối cùng [5, 3]. Tiêu chí nghiệm thu: bạn nói được thân lambda chạy bao nhiêu lần trong removeIf và vì sao. Gợi ý: removeIf gọi điều kiện đúng một lần cho mỗi phần tử.
Bài 2. Chọn interface và viết lambda. Dữ liệu vào là bốn việc: (a) kiểm tra đơn có đồ uống là Tea; (b) từ đơn lấy tên đồ uống viết hoa; (c) in một đơn lên màn hình với tiền tố screen: ; (d) tạo một đơn mặc định Water:3. Với mỗi việc, ghi tên interface và viết lambda. Kết quả mong đợi: bốn interface khác nhau. Tiêu chí nghiệm thu: mỗi lambda khớp với số tham số và kiểu trả về của interface. Gợi ý: nhìn vào hình "Which functional interface for which job".
Bài 3. Sửa lỗi biến bắt giữ. Dữ liệu vào:
int total = 0;
orders.forEach(o -> total += o.priority);
Sửa để tính đúng tổng ưu tiên của danh sách. Kết quả mong đợi: tổng bằng 7 với bốn đơn Latte:2, Mocha:1, Tea:3, Espresso:1. Tiêu chí nghiệm thu: không còn lỗi biên dịch và bạn giải thích được vì sao dòng gốc bị từ chối. Gợi ý: một vòng for thông thường không có hạn chế này.
Mức Vận dụng
Bài 4. Màn hình bếp nhận lambda từ ngoài. Viết class OrderBoard giữ một List<Order> và có ba phương thức nhận lambda:
| Phương thức | Việc phải làm |
|---|---|
int count(Predicate<Order> rule) | Đếm số đơn thoả điều kiện |
List<String> labels(Function<Order, String> toLabel) | Trả danh sách nhãn, mỗi đơn một nhãn |
void forEachOrder(Consumer<Order> action) | Chạy hành động cho từng đơn |
Dữ liệu vào là hàm main sau:
OrderBoard board = new OrderBoard();
board.add(new Order("Latte", 2));
board.add(new Order("Mocha", 1));
board.add(new Order("Tea", 3));
board.add(new Order("Espresso", 1));
System.out.println(board.count(o -> o.priority == 1));
System.out.println(board.labels(o -> o.drink + "#" + o.priority));
board.forEachOrder(o -> System.out.println("screen: " + o));
Kết quả mong đợi:
2
[Latte#2, Mocha#1, Tea#3, Espresso#1]
screen: Latte:2
screen: Mocha:1
screen: Tea:3
screen: Espresso:1
Tiêu chí nghiệm thu: sáu dòng khớp từng ký tự, và ba phương thức không biết gì về nội dung lambda mà chúng nhận. Gợi ý: mỗi phương thức chỉ là một vòng for gọi test, apply hoặc accept.
Mức Thử thách (tuỳ chọn)
Bài 5. Ghép điều kiện. Dữ liệu vào: năm đơn Latte:2, Mocha:1, Tea:3, Espresso:1, Latte:1 và hai điều kiện urgent (ưu tiên 1) và latte (đồ uống là Latte). Dùng and và negate của Predicate để đếm các đơn khẩn mà không phải Latte. Kết quả mong đợi: 2. Tiêu chí nghiệm thu: không viết thêm lambda thứ ba, chỉ ghép hai lambda có sẵn. Gợi ý: urgent.and(...) nhận một Predicate khác.
Tự kiểm tra (Retrieval Practice)
Trả lời bằng trí nhớ, không xem lại bài. Mỗi câu ghi rõ nó kiểm tra mục tiêu nào trong bốn mục tiêu ở đầu bài.
Câu 1 (nhớ lại, mục tiêu 2). Functional interface là gì? Kể tên bốn functional interface hay dùng cùng phương thức của chúng.
Câu 2 (dự đoán kết quả code, mục tiêu 3). Đoạn sau in ra gì?
List<String> names = new ArrayList<>(List.of("Tea", "Latte", "Mocha", "Chai"));
names.removeIf(s -> s.length() > 4);
names.forEach(s -> System.out.print(s.charAt(0)));
Câu 3 (dự đoán trạng thái sau nhiều bước, mục tiêu 2). Sau mỗi dòng dưới đây, log.size() bằng bao nhiêu?
List<Integer> log = new ArrayList<>();
Runnable r = () -> log.add(1);
r.run();
r.run();
Câu 4 (tìm lỗi, mục tiêu 4). Đoạn sau không biên dịch được. Chỉ ra lỗi và viết lại cho đúng.
int total = 0;
orders.forEach(o -> total += o.priority);
Câu 5 (giải thích cho bạn chưa học, mục tiêu 1 và 2). Bạn của bạn hỏi: "Lambda là gì mà cứ thấy dấu mũi tên?" Giải thích bằng ví dụ tờ phiếu hướng dẫn cho nhân viên bếp.
Điều cần nhớ
- Lambda là cách viết gọn cho phương thức duy nhất của một functional interface, như
Predicate(test),Function(apply),Consumer(accept),Supplier(get) (mục tiêu 1). - Functional interface có đúng một phương thức trừu tượng, và thân lambda chỉ chạy khi có ai gọi phương thức đó (mục tiêu 2).
removeIf,forEach,sortnhận lambda, nên một vòng lặp quen thuộc chỉ còn một dòng (mục tiêu 3).- Biến local được lambda dùng phải effectively final vì lambda giữ bản sao giá trị, còn đối tượng bắt giữ thì sửa được nội dung (mục tiêu 4).
Cheat sheet một trang.
| Việc cần làm | Interface | Phương thức | Ví dụ lambda |
|---|---|---|---|
| Kiểm tra đúng hoặc sai | Predicate<T> | test | o -> o.priority == 1 |
| Đổi một giá trị thành giá trị khác | Function<T, R> | apply | o -> o.drink.toUpperCase() |
| Làm một việc với giá trị | Consumer<T> | accept | s -> System.out.println(s) |
| Tạo giá trị khi cần | Supplier<T> | get | () -> new Order("Water", 3) |
| So hai giá trị | Comparator<T> | compare | (a, b) -> Integer.compare(a, b) |
| Đảo điều kiện | predicate.negate() | trả Predicate | urgent.negate() |
| Ghép hai điều kiện | p1.and(p2), p1.or(p2) | trả Predicate | urgent.and(latte) |
Cách đọc hình: mỗi hàng là một việc bên trái và interface nên chọn bên phải, kèm các phương thức thường dùng. Đọc theo hàng ngang. Dòng ghi chú cuối nhắc String::length là cách viết gọn của lambda chỉ gọi một phương thức.
Câu hỏi để dành cho ngày mai: bạn vừa lọc và đổi nhãn bằng vòng for kèm lambda. Nếu muốn viết cả chuỗi "lọc đơn khẩn, đổi thành nhãn, gom thành danh sách" chỉ trong một câu liền mạch, không cần danh sách trung gian nào, bạn nghĩ nó sẽ trông thế nào? Hãy nghĩ trước khi sang bài sau.
Đào sâu
Phần này dành cho bạn tò mò. Bỏ qua cũng không thiếu gì ở trên.
Bên trong. Lambda không chỉ là cách viết gọn của lớp ẩn danh. Một khác biệt có thể đo được là từ khoá this. Trong lớp ẩn danh, this trỏ tới chính đối tượng ẩn danh. Trong lambda, this trỏ tới đối tượng bao quanh. Trong một lần thử, cùng một lớp có trường v bằng 7 và lớp ẩn danh có trường v bằng 99: lambda in 7, lớp ẩn danh in 99 [Đã chạy, JDK 17.0.15].
So với lựa chọn khác. Khi lambda chỉ gọi đúng một phương thức, bạn có thể dùng method reference: String::length thay cho s -> s.length(), System.out::println thay cho s -> System.out.println(s), ArrayList::new thay cho () -> new ArrayList<>() [Đã chạy, cả ba đều biên dịch và chạy]. Chúng ngắn hơn nhưng không thay được lambda khi bạn cần thêm logic. Ngoài ra Function có andThen và compose: với add là x -> x + 10 và twice là x -> x * 2, add.andThen(twice).apply(5) cho 30 còn add.compose(twice).apply(5) cho 20 [Đã chạy].
Hiểu lầm nâng cao. Quy tắc effectively final chỉ áp dụng cho biến local và tham số. Trường của đối tượng thì lambda đọc và sửa tự do. Nhiều người lách quy tắc bằng cách bọc biến vào một mảng một phần tử rồi tăng phần tử đó. Cách này biên dịch được, nhưng nó che đi vấn đề thật là một lambda đang âm thầm thay đổi trạng thái bên ngoài, và sẽ gây lỗi khó thấy khi nhiều luồng cùng chạy. Bài này không khuyến khích cách đó.
Giới hạn của bài. Bài này chưa nói về BiFunction, UnaryOperator, BinaryOperator, các interface cho kiểu nguyên thuỷ như IntPredicate, về cách lambda xử lý ngoại lệ có kiểm tra (checked exception), và cách chuỗi lambda kết hợp với Stream ở bài sau. Số liệu trong bài thuộc JDK 17.0.15.
Đọc thêm (tài liệu chính thống, Java SE 17):
- The Java Tutorials: Lambda Expressions
- Javadoc: package java.util.function
- Javadoc: java.lang.FunctionalInterface
- Javadoc: java.util.function.Predicate
Bài tiếp theo
Bài sau là Stream API: nối nhiều lambda thành một chuỗi xử lý liền mạch như lọc, đổi, gom, mà không cần danh sách trung gian nào.
Mini-dự án nối tiếp: màn hình bếp sẽ thống kê đơn bằng Stream: đếm đơn khẩn, gom đồ uống theo ưu tiên, tính tổng. Hãy giữ lại class Order và OrderBoard có ba phương thức nhận lambda bạn vừa viết làm điểm xuất phát.
Lời giải và đáp án
Hãy thử xong rồi mới đọc phần này.
Lời giải Làm cùng mình. Thay các dòng // TODO bằng những dòng dưới đây, ghép lại chạy ra đúng năm dòng step [Đã chạy, JDK 17.0.15]:
Predicate<String> isLong = s -> s.length() > 4;
Function<String, Integer> len = s -> s.length();
names.forEach(n -> initials.append(n.charAt(0))); // initials không bị gán lại
nums.removeIf(n -> n > limit); // limit chỉ gán một lần
names.sort((a, b) -> Integer.compare(b.length(), a.length())); // đảo vai a và b
| Dòng code | Máy làm gì | Lý do chọn cách này |
|---|---|---|
s -> s.length() > 4 | Trả true khi chuỗi dài hơn 4 ký tự | Predicate cần kết quả boolean, và biểu thức so sánh đã là boolean |
s -> s.length() | Trả độ dài của chuỗi | Function<String, Integer> nhận chuỗi, trả số nguyên |
names.forEach(n -> initials.append(n.charAt(0))) | Với mỗi tên, nối chữ cái đầu vào initials | initials là đối tượng được bắt giữ, ta chỉ sửa nội dung nên hợp lệ |
nums.removeIf(n -> n > limit) | Xoá mọi số lớn hơn limit | Lambda dùng limit được vì nó chỉ gán một lần |
(a, b) -> Integer.compare(b.length(), a.length()) | Đặt tên dài hơn lên trước | Đảo a và b biến thứ tự tăng thành giảm, Latte và Mocha hoà nên giữ nguyên thứ tự cũ |
Lời giải Bài 1. [Suy luận, khớp với kết quả chạy thật [5, 3]]
| n | big.test(n) | Còn hay xoá |
|---|---|---|
| 5 | false | còn |
| 8 | true | xoá |
| 3 | false | còn |
| 10 | true | xoá |
Thân lambda chạy 4 lần trong removeIf, mỗi phần tử một lần. Danh sách cuối là [5, 3], và đoạn code sau đó cho log là 5;3;.
Lời giải Bài 2. [Đã chạy] (a) Predicate<Order> isTea = o -> o.drink.equals("Tea"); (b) Function<Order, String> up = o -> o.drink.toUpperCase(); (c) Consumer<Order> show = o -> System.out.println("screen: " + o); (d) Supplier<Order> def = () -> new Order("Water", 3);. Bốn lambda có số tham số khác nhau (một, một, một, không) và kiểu trả về khác nhau (boolean, String, không có, Order), nên cần bốn interface khác nhau.
Lời giải Bài 3. [Đã chạy, tổng bằng 7]
int total = 0;
for (Order o : orders) {
total += o.priority; // vòng for thường có thể gán lại total
}
Dòng gốc bị từ chối vì total += ... gán lại biến total ngay trong lambda, mà lambda chỉ giữ bản sao nên không thể đổi biến gốc. Vòng for thường không có lambda nên không có giới hạn này.
Lời giải Bài 4. [Đã chạy, sáu dòng khớp với kết quả mong đợi]
public int count(Predicate<Order> rule) {
int n = 0;
for (Order o : orders) {
if (rule.test(o)) {
n++;
}
}
return n;
}
public List<String> labels(Function<Order, String> toLabel) {
List<String> out = new ArrayList<>();
for (Order o : orders) {
out.add(toLabel.apply(o));
}
return out;
}
| Phương thức | Máy làm gì | Lý do chọn cách này |
|---|---|---|
count | Gọi rule.test(o) cho từng đơn và đếm số true | Phương thức không biết điều kiện là gì, chỉ hỏi test |
labels | Gọi toLabel.apply(o) cho từng đơn rồi gom kết quả | Hành vi đổi nhãn do người gọi quyết định |
forEachOrder | Gọi action.accept(o) cho từng đơn | Chỉ khác ở chỗ gọi accept và không trả gì |
Lời giải Bài 5. [Đã chạy, kết quả 2] Ghép bằng Predicate<Order> rule = urgent.and(latte.negate());, rồi đếm các đơn mà rule.test(o) là true. Trong năm đơn, chỉ Mocha:1 và Espresso:1 vừa khẩn vừa không phải Latte, còn Latte:1 khẩn nhưng là Latte nên bị loại.
Đáp án Tự kiểm tra.
- Functional interface là interface có đúng một phương thức trừu tượng. Bốn interface hay dùng:
Predicate(test),Function(apply),Consumer(accept),Supplier(get). Kiểm tra mục tiêu 2. - In ra
TC[Đã chạy].removeIfxoá Latte và Mocha (dài 5), còn Tea và Chai, rồiforEachin chữ cái đầu của mỗi tên còn lại. Kiểm tra mục tiêu 3. - Sau dòng tạo
r: 0 (lambda chưa chạy). Saur.run()lần một: 1. Saur.run()lần hai: 2 [Đã chạy: 0 rồi 2]. Kiểm tra mục tiêu 2. - Lỗi:
total += ...gán lại biến local trong lambda, vi phạm effectively final. Viết lại bằng vòngforthường, hoặc dùng mộtStringBuilderhay mảng, nhưng vòngforrõ ràng nhất. Kiểm tra mục tiêu 4. - Lambda là một tờ phiếu hướng dẫn ngắn bạn đưa cho nhân viên bếp, ví dụ "đơn nào ưu tiên 1 thì in lên màn hình". Dấu
->ngăn cách phần đầu vào ("đơn nào") và phần việc phải làm. Nhân viên cầm phiếu và làm theo cho từng đơn, còn bạn không phải đứng bên cạnh. Kiểm tra mục tiêu 1 và 2.