Method reference là cách viết tắt của lambda: s -> s.toUpperCase() thành String::toUpperCase.
Nghe đơn giản, nhưng có một chỗ khiến người mới bối rối — và hôm nay ta gỡ đúng chỗ đó.
Hai dòng gần giống nhau, hai ý nghĩa khác hẳn
ds.stream().map(String::toUpperCase)
ds.stream().map(tienTo::concat)
String::toUpperCase : [JAVA, KOTLIN]
tienTo::concat : [Ngôn ngữ java, Ngôn ngữ kotlin]
Cùng dạng X::y, nhưng:
Ở dòng đầu, String là tên lớp, và phần tử của stream trở thành đối tượng được gọi phương thức. Tức s -> s.toUpperCase().
Ở dòng sau, tienTo là một biến, và phần tử của stream trở thành tham số. Tức s -> tienTo.concat(s).
Trình biên dịch phân biệt bằng việc X là tên lớp hay tên biến. Đây là nguồn bối rối lớn nhất khi mới dùng, nên đáng dừng lại một nhịp.
Bốn dạng
Một — phương thức tĩnh: TenLop::phuongThucTinh
Function<String,Integer> f = Integer::parseInt; // s -> Integer.parseInt(s)
BinaryOperator<String> g = Mr::noiDai; // (a,b) -> Mr.noiDai(a,b)
Integer::parseInt("42") = 42
Mr::noiDai("a","b") = a-b
Mọi tham số của lambda thành tham số của phương thức. Dạng dễ hiểu nhất.
Hai — phương thức của một đối tượng cụ thể: bien::phuongThuc
Nguoi minh = new Nguoi("Minh", 30);
Supplier<String> f = minh::chao; // () -> minh.chao()
minh::chao() = Xin chào Minh
Đối tượng đã được "gắn" sẵn. Người ta gọi đây là bound reference.
Lưu ý: đối tượng được lấy ngay lúc tạo method reference, không phải lúc gọi. Nếu sau đó minh được gán lại thì method reference vẫn giữ đối tượng cũ — khác hẳn với lambda đọc một trường, như bài lambda đã chỉ ra.
Ba — phương thức của đối tượng bất kỳ cùng kiểu: TenLop::phuongThucThucThe
Function<String,String> f = String::toUpperCase; // s -> s.toUpperCase()
BiFunction<String,String,Boolean> g = String::startsWith; // (a,b) -> a.startsWith(b)
String::toUpperCase("java") = JAVA
String::startsWith("java","ja") = true
Đây là dạng gây nhầm. Tham số đầu tiên trở thành this, các tham số còn lại là tham số thật của phương thức. Nên BiFunction hai tham số lại ánh xạ tới một phương thức chỉ nhận một tham số.
Bốn — hàm khởi tạo: TenLop::new
Supplier<ArrayList<String>> f = ArrayList::new;
IntFunction<String[]> g = String[]::new; // mảng cũng được
ArrayList::new = []
String[]::new (3) = độ dài 3
Dạng mảng String[]::new trông lạ nhưng dùng rất thường xuyên:
toArray(String[]::new) -> [java, kotlin]
toArray() không tham số -> kiểu Object[]
Không truyền gì thì toArray() trả Object[] — hầu như luôn không phải thứ bạn muốn.
Khi javac không quyết được
Nếu một lớp có cả phương thức tĩnh lẫn phương thức thể hiện cùng tên mà cả hai đều khớp:
static class A {
static String tao(A x) { return "static"; } // hợp Function<A,String>
String tao() { return "instance"; } // cũng hợp
}
Function<A,String> f = A::tao;
error: incompatible types: invalid method reference
reference to tao is ambiguous
both method tao(A) in A and method tao() in A match
Trình biên dịch không đoán. Cách chữa: viết lambda tường minh — a -> A.tao(a) hoặc a -> a.tao().
Tình huống này hiếm, nhưng khi gặp thì thông báo lỗi khá khó hiểu nếu chưa biết bốn dạng ở trên.
Ở đâu method reference thật sự đáng dùng
Chỗ nó toả sáng nhất là Comparator:
nds.sort(Comparator.comparing(Nguoi::ten));
nds.sort(Comparator.comparingInt(Nguoi::tuoi));
theo tên : [An, Binh, Cuong]
theo tuổi : [Binh, Cuong, An]
Và trong chuỗi Stream, khi mỗi bước là một phép biến đổi đơn giản:
ds.stream().map(Nguoi::ten).filter(Objects::nonNull).sorted().toList()
Bốn method reference liên tiếp, đọc gần như một câu.
Khi nào KHÔNG nên rút gọn
Đây là phần tôi thấy ít người nói tới. Method reference ngắn hơn, nhưng không phải lúc nào cũng rõ hơn.
Khi tên phương thức không nói lên việc nó làm ở ngữ cảnh này:
.map(Nguoi::xuLy) // xử lý gì?
.map(n -> tinhLuong(n)) // rõ hơn dù dài hơn
Khi mất thông tin về kiểu. Với lambda, n -> n.ten() cho người đọc biết ngay tham số là gì. Với Nguoi::ten thì phải suy từ ngữ cảnh — trong chuỗi Stream dài, đôi khi không dễ.
Khi bạn phải dừng lại nghĩ nó thuộc dạng nào. Nếu chính bạn mất ba giây để xác định X::y là gọi trên tham số hay gọi trên biến, thì người đọc mã cũng vậy. Lambda tường minh không có nhập nhằng đó.
Khi cần thêm bất cứ gì. Ngay khi phải kiểm tra null, ghi log, hay bọc try-catch, method reference không dùng được nữa — và cố nhồi vào bằng một hàm phụ đôi khi tệ hơn viết lambda.
Quy tắc của tôi: method reference cho những phép biến đổi mà tên phương thức đã nói hết; lambda cho mọi thứ còn lại.
Vài mẹo nhỏ
Objects::nonNull và Objects::isNull dùng liên tục trong filter.
String::valueOf chuyển bất cứ gì thành chuỗi, an toàn với null (trả về chuỗi "null").
Map.Entry::getKey và getValue khi làm việc với entrySet().stream().
this::phuongThuc gọi được phương thức của chính lớp hiện tại — và nhớ rằng nó bắt this, nên giữ đối tượng sống, đúng cảnh báo ở bài lambda.
super::phuongThuc cũng hợp lệ, dùng khi ghi đè mà vẫn cần bản của lớp cha.
Thử ba mươi giây
Viết Function<String,String> f = String::trim; rồi String s = " x "; Function<String,String> g = s::concat;
Cả hai đều là X::y. Áp dụng cả hai lên "abc" và xem kết quả — một cái gọi trim trên "abc", cái kia dùng "abc" làm tham số. Hiểu được khác biệt đó là hiểu xong method reference.
Ngày mai ta bắt đầu Stream: nguồn, phép trung gian, phép kết — và vì sao một chuỗi Stream không làm gì cả cho tới khi bạn gọi phép cuối.