Biểu thức Lambda
Khi cần truyền một mảnh hành vi cho phương thức khác, cách cũ là tạo một lớp ẩn danh
(anonymous class) cài đặt giao tiếp đó. Cách viết này dài dòng: phần lớn dòng mã chỉ
là khung lớp, còn phần thật sự quan trọng là thân phương thức lại nằm lọt giữa. Biểu
thức Lambda (lambda expression) ra đời từ Java 8 để cắt bỏ khung thừa ấy, chỉ giữ
lại danh sách tham số và thân hàm, viết dưới dạng (tham số) -> thân hàm. Lambda là một
thể hiện của một giao tiếp hàm, nên gán được cho biến hoặc truyền làm đối số ở bất kỳ nơi
nào trông đợi giao tiếp hàm đó.
Hãy hình dung một tình huống quen thuộc: bạn có danh sách sinh viên và muốn sắp xếp theo điểm, rồi lát sau theo tên, rồi theo tuổi. Nếu mỗi tiêu chí đòi một lớp ẩn danh riêng, mã phình lên nhanh chóng và phần logic thật (một phép so sánh) bị chôn giữa hàng chục dòng khung lớp. Lambda cho phép truyền thẳng phép so sánh đó như một giá trị, đúng chỗ cần dùng: hành vi trở thành thứ ta lưu, truyền và ghép nối được, không còn bị trói vào một lớp có tên.
Sâu xa hơn, Lambda đưa vào Java một ý niệm quen thuộc của các ngôn ngữ hàm: hành vi cũng là
một giá trị. Cần nói ngay một điểm để tránh hiểu lầm: Lambda không phải kiểu dữ liệu mới.
Mỗi biểu thức Lambda khi biên dịch đều phải quy về một thể hiện của một giao tiếp hàm cụ thể,
và đó là lý do var f = x -> x * x; không biên dịch được.
Thử ngay: xem Lambda chạy từng bước
Chưa cần đọc hết lý thuyết, bấm ▶ Chạy để theo dõi hai Lambda viết cùng một dòng mã
() -> n nhưng bắt hai giá trị khác nhau. Điểm mấu chốt: mỗi Lambda chép lại giá trị biến
tại thời điểm nó được tạo, và giữ nguyên giá trị đó khi được gọi về sau, rất lâu sau khi
phương thức make đã kết thúc. Phần lý thuyết ở dưới sẽ giải thích chuyện bắt biến này.
1public class Demo {2 static java.util.function.Supplier<Integer> make(int n) {3 return () -> n; // capture n when the lambda is created4 }56 public static void main(String[] args) {7 java.util.function.Supplier<Integer> a = make(5);8 java.util.function.Supplier<Integer> b = make(20);9 int x = a.get(); // called later, still returns 510 int y = b.get(); // returns 2011 System.out.println(x + " " + y);12 }13}
Cú pháp
Một biểu thức Lambda gồm ba phần: danh sách tham số trong ngoặc đơn, mũi tên ->, và
thân hàm. Thân hàm có thể là một biểu thức đơn (giá trị của biểu thức chính là giá trị
trả về), hoặc một khối lệnh trong cặp ngoặc nhọn (khi đó dùng return tường minh nếu cần
trả về).
// 1. one parameter, single expression body
Function<Integer, Integer> square = x -> x * x;
// 2. parentheses optional for exactly one parameter
Consumer<String> printer = s -> System.out.println(s);
// 3. multiple parameters require parentheses
BinaryOperator<Integer> add = (a, b) -> a + b;
// 4. no parameter: empty parentheses
Supplier<Double> rng = () -> Math.random();
// 5. block body with explicit return
Comparator<String> byLength = (a, b) -> {
int diff = a.length() - b.length(); // compute the gap
return Integer.compare(diff, 0); // need return in a block
};
// 6. explicit parameter types (rarely needed)
BiFunction<Integer, Integer, Integer> mul = (Integer a, Integer b) -> a * b;
Trình biên dịch thường tự suy ra kiểu tham số từ giao tiếp hàm đích, nên ta hiếm khi
viết kiểu tường minh như dòng cuối. Khi chỉ có đúng một tham số và không ghi kiểu, có thể
bỏ luôn ngoặc đơn. Khi thân hàm là một biểu thức đơn, ta không viết return: chính giá
trị của biểu thức là kết quả trả về.
Suy luận kiểu hoạt động ra sao
Trong x -> x * x, trình biên dịch dựa vào đâu để biết x là số nguyên? Câu trả lời nằm ở
kiểu đích (target type), tức kiểu của chỗ mà Lambda được đặt vào. Trình biên dịch tìm
giao tiếp hàm đích, đọc chữ ký phương thức trừu tượng duy nhất của nó là R apply(T t), thay
tham số kiểu vào được Integer apply(Integer t), nên x phải là Integer.
Cơ chế này giải thích vì sao var f = x -> x * x; báo lỗi: var suy kiểu từ vế phải còn
Lambda suy kiểu từ vế trái, hai bên cùng chờ nhau. Hệ quả thực dụng: khi Lambda báo lỗi kiểu
khó hiểu, hãy soi kiểu đích trước, đừng soi thân Lambda.
Lambda chỉ áp dụng được cho giao tiếp hàm (đúng một phương thức trừu tượng). Nếu giao
tiếp có hai phương thức trừu tượng trở lên, trình biên dịch không biết Lambda cài đặt
phương thức nào và sẽ báo lỗi. Hãy gắn @FunctionalInterface cho giao tiếp tự định nghĩa
để trình biên dịch kiểm tra ràng buộc này giúp.
So sánh với lớp ẩn danh
Đặt cạnh nhau hai cách viết cùng một Comparator để thấy Lambda gọn tới mức nào.
List<String> names = new ArrayList<>(List.of("An", "Binh", "Cuong"));
// (a) anonymous class: verbose, the real logic is one line
names.sort(new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
});
// (b) lambda: same behaviour, the boilerplate disappears
names.sort((a, b) -> a.length() - b.length());
Cả hai cùng tạo một đối tượng cài đặt Comparator<String>. Khác biệt nằm ở chỗ Lambda
lược bỏ tên lớp, tên phương thức, chú giải @Override và các dấu ngoặc nhọn không cần
thiết. Ngoài hình thức, hai cách còn khác về ngữ nghĩa 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 của lớp bao
quanh. Về biên dịch, lớp ẩn danh sinh ra một tệp .class riêng, còn Lambda được biên
dịch thành một phương thức ẩn trong lớp hiện tại và dùng cơ chế invokedynamic, nhẹ hơn
về số lớp sinh ra.
Dùng Lambda khi giao tiếp đích là giao tiếp hàm và ta chỉ cần một thân hàm ngắn. Giữ lớp
ẩn danh khi cần cài nhiều phương thức, cần trường trạng thái riêng, hoặc cần this trỏ
tới chính đối tượng ấy.
Biến bắt
Thân Lambda đọc được các biến cục bộ của phương thức bao quanh; hiện tượng này gọi là
bắt biến (variable capture). Tuy nhiên Java đặt một điều kiện: biến cục bộ bị bắt
phải là gần như bất biến (effectively final), nghĩa là sau khi gán giá trị lần đầu
thì không được gán lại. Ta không bắt buộc thêm từ khoá final, nhưng giá trị biến phải
không đổi trong suốt phạm vi đó.
int base = 100; // effectively final
Function<Integer, Integer> addBase = x -> x + base; // captures base
System.out.println(addBase.apply(5)); // prints 105
// base = 200; // COMPILE ERROR: base would no longer be effectively final
Vì sao Java đòi điều kiện này? Một Lambda có thể được lưu lại và chạy về sau, thậm chí ở một luồng khác, khi phương thức tạo ra nó đã kết thúc và biến cục bộ đã biến mất khỏi ngăn xếp. Để an toàn, Java sao chép giá trị biến vào Lambda tại thời điểm tạo. Nếu cho phép biến thay đổi, bản sao trong Lambda và bản gốc sẽ lệch nhau, gây nhầm lẫn khó lường. Buộc biến bất biến giúp loại bỏ rủi ro đó.
Giao tiếp hàm là gì và vì sao Lambda cần nó
Giao tiếp hàm (functional interface) là interface có đúng một phương thức trừu
tượng. Nó vẫn chứa được bao nhiêu phương thức default và static tuỳ thích, vì những phương
thức ấy đã có thân hàm nên không cần ai cài đặt. Ràng buộc "đúng một" chỉ áp cho phương thức
trừu tượng, và đó là lý do Comparator vẫn là giao tiếp hàm dù có hàng chục phương thức: chỉ
compare là trừu tượng.
Vì sao Lambda cần điều kiện đó? Hãy đặt mình vào vị trí trình biên dịch: nó nhận thân Lambda cùng một kiểu đích, và phải lấy thân ấy làm thân của một phương thức. Nhưng phương thức nào? Nếu kiểu đích có hai phương thức trừu tượng, không quy tắc nào cho biết Lambda ứng với cái nào, và Java không đoán mò. Điều kiện này vì vậy là mức tối thiểu để phép ánh xạ có nghĩa. Ta vẫn tự viết giao tiếp hàm khi muốn đặt tên cho khái niệm nghiệp vụ.
@FunctionalInterface
interface TinhPhiVanChuyen {
double tinh(double khoiLuong, double khoangCach); // the single abstract method
// a default method is allowed: it already has a body
default double tinhCoPhuPhi(double khoiLuong, double khoangCach) {
return tinh(khoiLuong, khoangCach) * 1.2; // 20 percent surcharge
}
}
public class DemoPhi {
public static void main(String[] args) {
TinhPhiVanChuyen thuong = (kl, kc) -> kl * 1000 + kc * 500;
System.out.println(thuong.tinh(2, 10)); // 7000.0
System.out.println(thuong.tinhCoPhuPhi(2, 10)); // 8400.0
}
}
Chú giải @FunctionalInterface không tạo phép màu nào: interface nào có đúng một phương thức
trừu tượng thì tự khắc đã là giao tiếp hàm. Công dụng thật của nó là làm hàng rào, vì nếu sau
này ai đó thêm một phương thức trừu tượng thứ hai, trình biên dịch báo lỗi ngay tại chỗ khai
báo thay vì để lỗi nổ ra ở nơi dùng Lambda.
Giao tiếp hàm chuẩn
Ta không phải tự định nghĩa giao tiếp hàm cho mỗi nhu cầu. Gói java.util.function cung
cấp sẵn một bộ giao tiếp hàm dùng chung, phủ hầu hết trường hợp thường gặp. Nhớ bốn nhóm
gốc là đủ dùng trong phần lớn công việc:
Function<T, R>: nhận mộtT, trả về mộtR; phương thức làapply.Supplier<T>: không nhận gì, trả về mộtT; phương thức làget.Consumer<T>: nhận mộtT, không trả về gì; phương thức làaccept.Predicate<T>: nhận mộtT, trả vềboolean; phương thức làtest.
Phần cú pháp phía trên đã dùng ba trong bốn nhóm này. Nhóm còn lại, Predicate, chuyên
cho phép kiểm tra điều kiện:
import java.util.function.*;
Predicate<Integer> chan = n -> n % 2 == 0;
System.out.println(chan.test(10)); // true
// ghep dieu kien bang and, or, negate
Predicate<Integer> duong = n -> n > 0;
Predicate<Integer> chanVaDuong = chan.and(duong);
System.out.println(chanVaDuong.test(-4)); // false
Ngoài bốn nhóm này còn có BiFunction<T, U, R> cho hai đối số, UnaryOperator<T> và
BinaryOperator<T> cho các phép trên cùng một kiểu, cùng các biến thể chuyên cho số
nguyên thủy như IntPredicate để tránh chi phí đóng hộp (boxing). Khi cần một giao tiếp
hàm, hãy dò trong gói này trước khi tự viết mới.
Tham chiếu phương thức
Khi thân Lambda chỉ gọi lại đúng một phương thức có sẵn, ta rút gọn thêm một bậc nữa bằng
tham chiếu phương thức (method reference), viết dưới dạng Lop::tenPhuongThuc. Đây
chỉ là cách viết ngắn hơn của một Lambda tương đương, không phải cơ chế mới; trình biên
dịch tự dựng lại Lambda từ tham chiếu đó.
List<String> names = new ArrayList<>(List.of("An", "Binh", "Cuong"));
// lambda goi mot phuong thuc tinh
names.forEach(s -> System.out.println(s));
// tham chieu phuong thuc tinh, gon hon
names.forEach(System.out::println);
// tham chieu phuong thuc thuc the theo kieu: goi length() tren tung phan tu
names.sort(Comparator.comparingInt(String::length));
// tham chieu ham dung: dung :: new de tao doi tuong
Supplier<ArrayList<String>> taoMoi = ArrayList::new;
Có đúng bốn dạng tham chiếu.
Dạng một, phương thức tĩnh, viết Lop::phuongThucTinh: Integer::parseInt tương đương
s -> Integer.parseInt(s).
Dạng hai, phương thức của một đối tượng cụ thể có sẵn, viết doiTuong::phuongThuc. Đối
tượng nhận đã cố định từ trước: System.out::println tương đương s -> System.out.println(s).
Dạng ba, phương thức thực thể theo kiểu, viết Lop::phuongThuc với phuongThuc là phương
thức thực thể chứ không tĩnh. Dạng này hay gây bối rối vì nhìn giống hệt dạng một. Khác biệt
nằm ở chỗ đối tượng nhận không được ghi ra: tham số đầu tiên của Lambda đóng vai đối tượng
nhận, các tham số còn lại mới là đối số. Vì vậy String::length tương đương s -> s.length(),
còn String::compareTo tương đương (a, b) -> a.compareTo(b).
Dạng bốn, tham chiếu hàm dựng, viết Lop::new, chẳng hạn ArrayList::new tương đương
() -> new ArrayList<>().
Tham chiếu đáng dùng khi nó làm mã rõ hơn, không phải khi nó làm mã ngắn hơn.
Dùng Lambda với Comparator và forEach
Hai nơi Lambda phát huy rõ nhất là sắp xếp bằng Comparator và duyệt bằng forEach.
Comparator có sẵn các phương thức tĩnh và mặc định để ghép tiêu chí, nhờ vậy ta dựng
phép so sánh nhiều tầng mà không viết một dòng if nào.
import java.util.*;
record SinhVien(String ten, double diem, int tuoi) {}
List<SinhVien> ds = new ArrayList<>(List.of(
new SinhVien("An", 8.0, 20),
new SinhVien("Binh", 8.0, 19),
new SinhVien("Cuong", 6.5, 21)));
// sap theo diem giam dan, cung diem thi theo tuoi tang dan
ds.sort(Comparator.comparingDouble(SinhVien::diem).reversed()
.thenComparingInt(SinhVien::tuoi));
// duyet va in tung phan tu bang forEach
ds.forEach(sv -> System.out.println(sv.ten() + " " + sv.diem()));
comparingDouble nhận một hàm trích khoá so sánh, reversed đảo chiều, còn
thenComparingInt bổ sung tiêu chí phụ khi tiêu chí chính bằng nhau. Cách ghép này đọc
gần như một câu tiếng Anh và tránh hẳn lỗi viết tay phép so sánh nhiều tầng.
Có một chi tiết rất dễ hiểu sai ở đây. reversed không đảo riêng tiêu chí đứng ngay trước
nó; nó đảo toàn bộ bộ so sánh đã dựng xong tính đến thời điểm nó được gọi. Ở đoạn mã trên,
ta dựng bộ so sánh theo điểm, đảo nó thành điểm giảm dần, rồi mới nối thêm tiêu chí tuổi tăng
dần. Nhưng nếu viết comparingDouble(SinhVien::diem).thenComparingInt(SinhVien::tuoi).reversed()
thì ta dựng bộ so sánh hai tầng rồi đảo cả hai tầng: tuổi cũng bị đảo thành giảm dần. Lỗi
này khó thấy, vì khi không ai trùng điểm thì hai cách viết cho kết quả giống hệt nhau; sai lệch
chỉ lộ ra lúc có trùng tiêu chí chính.
this trong Lambda khác trong lớp ẩn danh
Trong lớp ẩn danh, this trỏ tới chính đối tượng ẩn danh, bởi đó thực sự là một lớp mới
có danh tính riêng. Trong Lambda, this trỏ tới đối tượng của lớp bao quanh, bởi Lambda
không tạo lớp mới; nó được biên dịch thành một phương thức ẩn nằm trong chính lớp bao quanh.
Người ta nói Lambda trong suốt về từ vựng (lexically scoped).
public class DemoThis {
private String ten = "lop bao quanh";
public void chay() {
// anonymous class: this refers to the anonymous object itself
Runnable a = new Runnable() {
private String ten = "lop an danh";
@Override
public void run() {
System.out.println(this.ten); // lop an danh
System.out.println(DemoThis.this.ten); // lop bao quanh
}
};
// lambda: this refers to the enclosing DemoThis object
Runnable b = () -> System.out.println(this.ten); // lop bao quanh
a.run();
b.run();
}
public static void main(String[] args) {
new DemoThis().chay();
}
}
Hai hệ quả thực tế. Thứ nhất, trong Lambda ta gọi thẳng phương thức và trường của lớp bao quanh. Thứ hai, Lambda không thể có trạng thái riêng vì nó không khai báo được trường; cần trạng thái nội bộ giữa các lần gọi thì lớp ẩn danh hoặc lớp có tên mới là công cụ đúng.
Khi nào không nên dùng Lambda
Khi thân hàm dài. Một Lambda mười lăm dòng đọc còn tệ hơn một phương thức có tên đàng hoàng. Lambda nên vừa một tới ba dòng; dài hơn thì tách thân thành phương thức riêng rồi trỏ tới nó bằng tham chiếu phương thức.
Khi cần trạng thái hoặc nhiều phương thức. Lambda không có trường, không có danh tính riêng. Cần những thứ đó thì dùng lớp ẩn danh hoặc lớp có tên.
Khi logic được dùng lại nhiều nơi. Cùng một phép lọc chép lại ở năm chỗ là nhân bản mã.
Hãy đặt nó thành một hằng static final hoặc một phương thức trả về Predicate.
Khi cần ném ngoại lệ có kiểm tra. Các giao tiếp hàm chuẩn không khai báo throws, nên
Lambda gọi phương thức ném ngoại lệ có kiểm tra sẽ không biên dịch được; ta buộc phải bọc
try catch ngay trong thân, và cách bọc đó thường làm mã xấu hơn một vòng lặp for thường.
- Gán lại biến đã bắt: mọi biến cục bộ mà Lambda đọc phải gần như bất biến, gán lại sẽ báo lỗi biên dịch.
- Nhầm
thistrong Lambda trỏ tới đối tượng Lambda: thực rathistrỏ tới đối tượng của lớp bao quanh, khác với lớp ẩn danh. - Dùng Lambda cho giao tiếp có nhiều hơn một phương thức trừu tượng: chỉ giao tiếp hàm mới nhận Lambda.
- Quên rằng thân biểu thức đơn không viết
return, còn thân dạng khối{ }thì bắt buộcreturnnếu cần trả giá trị.
Gán lại biến đã bắt. Gốc rễ là Lambda sao chép giá trị chứ không chia sẻ ô nhớ, và nó
có thể sống lâu hơn phương thức tạo ra nó. Muốn tích luỹ trạng thái, hãy dùng AtomicInteger.
Nhầm ý nghĩa của this. Gốc rễ là lầm tưởng Lambda tạo ra một lớp mới. Nó không tạo, nên
this giữ nguyên ý nghĩa cũ.
Nhầm dạng tham chiếu phương thức. String::length và Integer::parseInt trông giống hệt
nhau nhưng thuộc hai dạng khác nhau. Hãy hỏi thêm: phương thức đó tĩnh hay thực thể. Nếu là
thực thể, tham số đầu tiên chính là đối tượng nhận.
Bài tập thực hành
Bài tập 1: ba dạng cú pháp Lambda
Viết ba Lambda gán cho ba giao tiếp hàm chuẩn: một Function<Integer, Integer> bình
phương đầu vào, một BinaryOperator<Integer> cộng hai số, và một Supplier<String> trả
về một chuỗi cố định. Với mỗi cái, chỉ ra vì sao dùng (hoặc bỏ) ngoặc đơn quanh tham số.
Lời giải chi tiết
Function có đúng một tham số nên bỏ được ngoặc đơn. BinaryOperator có hai tham số nên
bắt buộc giữ ngoặc đơn. Supplier không có tham số nên phải viết ngoặc đơn rỗng.
import java.util.function.*;
Function<Integer, Integer> square = x -> x * x; // one param, no parens needed
BinaryOperator<Integer> add = (a, b) -> a + b; // two params, parens required
Supplier<String> hello = () -> "Hello, DAU"; // no param, empty parens
System.out.println(square.apply(6)); // 36
System.out.println(add.apply(3, 4)); // 7
System.out.println(hello.get()); // Hello, DAU
Ý chính: số tham số quyết định quy tắc ngoặc đơn; thân biểu thức đơn không cần return.
Bài tập 2: gần như bất biến
Cho đoạn mã sau và chỉ ra dòng gây lỗi biên dịch, giải thích vì sao.
int counter = 0;
Runnable r = () -> System.out.println(counter);
counter = 1; // ?
Lời giải chi tiết
Dòng counter = 1; gây lỗi biên dịch. Khi Lambda bắt counter, biến này phải gần như bất
biến; nhưng gán lại counter = 1 sau đó khiến nó không còn bất biến. Cách sửa là
không gán lại biến đã bắt, hoặc sao chép sang một biến mới bất biến trước khi bắt.
int counter = 0;
final int captured = counter; // effectively final copy
Runnable r = () -> System.out.println(captured); // captures the copy safely
r.run(); // prints 0
Ý chính: Lambda sao chép giá trị lúc tạo, nên Java buộc biến bắt phải bất biến để bản sao và bản gốc không lệch nhau.
Bài tập 3: sắp xếp bằng Comparator ghép tiêu chí
Cho danh sách List<SinhVien> với record SinhVien(String ten, double diem). Hãy sắp
xếp theo điểm giảm dần, nếu bằng điểm thì theo tên tăng dần theo bảng chữ cái, rồi in ra
bằng tham chiếu phương thức.
Lời giải chi tiết
Ghép Comparator.comparingDouble với reversed cho tiêu chí chính, rồi thenComparing
cho tiêu chí phụ. In bằng tham chiếu phương thức System.out::println.
import java.util.*;
record SinhVien(String ten, double diem) {}
List<SinhVien> ds = new ArrayList<>(List.of(
new SinhVien("Binh", 8.0),
new SinhVien("An", 8.0),
new SinhVien("Cuong", 6.5)));
ds.sort(Comparator.comparingDouble(SinhVien::diem).reversed()
.thenComparing(SinhVien::ten)); // diem giam, cung diem thi ten tang
ds.forEach(System.out::println); // in bang tham chieu phuong thuc
Ý chính: reversed đảo chiều toàn bộ bộ so sánh đã dựng tính tới chỗ nó được gọi, chứ không
riêng tiêu chí cuối. Vì vậy phải gọi nó ngay sau comparingDouble rồi mới thenComparing;
nếu viết comparingDouble(...).thenComparing(...).reversed() thì cả hai tiêu chí đều bị đảo,
tên sẽ xếp giảm dần chứ không tăng dần. thenComparing chỉ xét khi các tiêu chí trước bằng
nhau.
Bài tập 4: bốn dạng tham chiếu phương thức
Đoạn mã dưới có một dòng không biên dịch được. Hãy chỉ ra dòng nào và giải thích theo bốn dạng tham chiếu phương thức.
import java.util.function.*;
public class BaiTap4 {
public static void main(String[] args) {
Function<String, Integer> a = String::length;
BiFunction<String, String, Integer> b = String::compareTo;
Supplier<String> d = String::toUpperCase; // ?
System.out.println(a.apply("abcd"));
}
}
Lời giải chi tiết
Dòng khai báo d không biên dịch được.
Hai dòng đầu hợp lệ và đều thuộc dạng ba: tham số đầu tiên đóng vai đối tượng nhận. Với
a, tham số String làm đối tượng nhận rồi gọi length(). Với b, tham số thứ nhất làm đối
tượng nhận, tham số thứ hai làm đối số của compareTo.
Dòng d sai vì Supplier<String> không có tham số nào. Không có tham số thì không có gì
để làm đối tượng nhận, mà toUpperCase() lại là phương thức thực thể, bắt buộc phải có một
String để gọi lên. Có hai cách sửa.
// fix 1: give it a parameter to act as the receiver (form 3)
Function<String, String> d1 = String::toUpperCase;
System.out.println(d1.apply("abc")); // ABC
// fix 2: bind a concrete receiver, then Supplier makes sense (form 2)
String nguon = "abc";
Supplier<String> d2 = nguon::toUpperCase; // receiver fixed to nguon
System.out.println(d2.get()); // ABC
Ý chính: cách sửa thứ nhất giữ dạng ba, dùng tham số làm đối tượng nhận. Cách sửa thứ hai
chuyển sang dạng hai, cố định đối tượng nhận là biến nguon; lúc này giao tiếp không cần tham
số nào nữa nên Supplier mới hợp lệ.
Câu hỏi tự kiểm
- 1Vì sao câu var f = x -> x * x; không biên dịch được?
- 2Bên trong một Lambda, this trỏ tới đối tượng nào?
- 3comparingDouble(SinhVien::diem).thenComparingInt(SinhVien::tuoi).reversed() sắp xếp như thế nào?
- 4Dùng cho Function<String, Integer>, tham chiếu String::length thuộc dạng nào?
Tóm tắt
Lambda cắt bỏ khung thừa của lớp ẩn danh, giữ lại đúng danh sách tham số và thân hàm. Nó không
phải kiểu dữ liệu mới: mỗi Lambda đều quy về một thể hiện của một giao tiếp hàm, và trình biên
dịch nhìn kiểu đích để suy ra kiểu tham số. Biến cục bộ bị bắt phải gần như bất biến, vì Lambda
sao chép giá trị và có thể chạy sau khi phương thức tạo ra nó đã kết thúc. Còn this thì trỏ
tới đối tượng của lớp bao quanh, khác hẳn lớp ẩn danh; đổi lại Lambda không có trạng thái riêng.
Lambda viết dạng (tham so) -> than ham, chỉ dùng được cho giao tiếp hàm có đúng một
phương thức trừu tượng. Trình biên dịch tự suy kiểu tham số, thân biểu thức đơn không cần
return. Biến cục bộ bị bắt phải gần như bất biến. Khi thân Lambda chỉ gọi lại một phương
thức có sẵn, rút gọn thành tham chiếu phương thức Lop::phuongThuc. Gói java.util.function
đã có sẵn Function, Supplier, Consumer, Predicate cho hầu hết nhu cầu, nên hãy dò
ở đó trước khi tự định nghĩa giao tiếp mới.