Chuyển tới nội dung chính

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.

Thuật ngữ chốt về Lambda

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.

Lambda bắt giá trị tại thời điểm tạo java
1public class Demo {
2 static java.util.function.Supplier<Integer> make(int n) {
3 return () -> n; // capture n when the lambda is created
4 }
5
6 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 5
10 int y = b.get(); // returns 20
11 System.out.println(x + " " + y);
12 }
13}
Ngăn xếp stack
main()
Bộ nhớ động heap
(trống)
Bắt đầu main(). Chưa có biến nào, bộ nhớ động còn trống. Ta sắp tạo hai Lambda bằng cách gọi make hai lần.
1/8

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ỉ cho giao tiếp hàm

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.

Khi nào dùng cái nào

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 defaultstatic 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ột T, trả về một R; phương thức là apply.
  • Supplier<T>: không nhận gì, trả về một T; phương thức là get.
  • Consumer<T>: nhận một T, không trả về gì; phương thức là accept.
  • Predicate<T>: nhận một T, 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>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.

Lỗi thường gặp với Lambda
  • 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 this trong Lambda trỏ tới đối tượng Lambda: thực ra this trỏ 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ộc return nếu cần trả giá trị.
Nguyên nhân gốc của ba lỗi hay gặp

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::lengthInteger::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

Kiểm tra nhanh: biểu thức Lambda0/4 đúngchưa trả lời
  1. 1Vì sao câu var f = x -> x * x; không biên dịch được?
  2. 2Bên trong một Lambda, this trỏ tới đối tượng nào?
  3. 3comparingDouble(SinhVien::diem).thenComparingInt(SinhVien::tuoi).reversed() sắp xếp như thế nào?
  4. 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.

Ghi nhớ

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.