Lớp ẩn danh
Lớp ẩn danh là một lớp không có tên, được khai báo và tạo thể hiện ngay tại chỗ cần dùng. Nó thường dùng để cài đặt nhanh một giao tiếp hoặc ghi đè một lớp khi ta chỉ cần đúng một thể hiện duy nhất.
Hãy hình dung một tình huống thật. Ta đang viết một nút bấm và cần gắn cho nó một hành động khi người dùng nhấn. Hành động đó chỉ dùng đúng một lần, ở đúng chỗ này, không nơi nào khác gọi lại. Nếu bắt buộc tách ra một lớp riêng đặt trong một tệp riêng, mã nguồn sẽ phình lên vì những lớp bé xíu chỉ sống một chỗ. Lớp ẩn danh sinh ra để giải quyết đúng nỗi phiền này: nó cho ta viết thân lớp ngay tại nơi cần, gắn liền với ngữ cảnh, và không phải nghĩ ra một cái tên cho thứ chỉ dùng một lần.
Lớp ẩn danh là một trong bốn kiểu lớp lồng của Java. Trước khi đi sâu vào nó, ta điểm qua cả bốn kiểu để thấy lớp ẩn danh nằm ở đâu và khác các kiểu kia thế nào. Hiểu được điều này giúp ta chọn đúng công cụ chứ không lạm dụng một kiểu cho mọi việc.
Thử ngay: xem lớp ẩn danh chạy từng bước
Chưa cần đọc hết lý thuyết, hãy xem trước một hiện tượng lạ. Ví dụ dưới đây tạo một lớp ẩn danh
bên trong phương thức thể hiện của lớp Box, cất nó lại, sửa dữ liệu của Box rồi mới cho nó
chạy. Nó in ra giá trị mới nhất chứ không phải giá trị lúc nó được tạo. Bấm ▶ Chạy để theo
dõi từng dòng: đối tượng lớp ẩn danh nằm trên bộ nhớ động và mang một trường ẩn trỏ về đối
tượng Box bao. Cái trường ẩn ấy là lý do của toàn bộ hiện tượng, và mục "Tham chiếu ngầm tới
đối tượng bao" ở dưới sẽ giải thích nó cặn kẽ.
1public class Demo {2 public static void main(String[] args) {3 Box b = new Box(7); // enclosing object on the heap4 Runnable r = b.makeTask(); // anonymous class captures b5 b.value = 42; // change the captured object later6 r.run(); // prints 42, not 77 }8}910class Box {11 int value;12 Box(int value) { this.value = value; }13 Runnable makeTask() {14 return new Runnable() { // anonymous class15 public void run() {16 System.out.println(value); // reads the enclosing Box field17 }18 };19 }20}
Khai báo tại chỗ
Khi cần một đối tượng cài đặt giao tiếp chỉ để dùng một lần, viết hẳn một lớp riêng là thừa.
Lớp ẩn danh cho phép vừa khai báo vừa khởi tạo trong một biểu thức. Cú pháp là new kèm tên
giao tiếp hoặc lớp, rồi mở thân lớp ngay sau cặp ngoặc.
interface XuLy { // secondary type: not public, one file
void chay(); // single abstract method
}
public class Demo {
public static void main(String[] args) {
XuLy viec = new XuLy() { // anonymous class, no separate file
@Override public void chay() {
System.out.println("Dang chay mot tac vu");
}
};
viec.chay();
}
}
Giao tiếp XuLy chỉ có đúng một phương thức trừu tượng. Loại giao tiếp như vậy gọi là giao
tiếp hàm, và toàn bộ khối lớp ẩn danh ở trên về sau viết gọn lại thành một biểu thức Lambda.
Vì lẽ đó, lớp ẩn danh chính là bậc thang dẫn vào chương 2.
Lớp ẩn danh truy cập được các biến cục bộ bao quanh nó, nhưng các biến ấy phải là final
hoặc thực chất không đổi, nghĩa là không được gán lại sau khi khởi tạo.
Bốn loại lớp lồng và khi nào dùng
Lớp lồng tĩnh khai báo với từ khóa static bên trong một lớp khác. Nó không giữ tham chiếu
tới thể hiện lớp bao, nên hành xử gần như một lớp độc lập, chỉ khác chỗ nó được gói bên trong
để gom nhóm về mặt tên gọi. Ta chọn nó khi một lớp phụ chỉ có ý nghĩa trong phạm vi lớp bao,
ví dụ lớp Node bên trong lớp DanhSachLienKet. Vì không giữ tham chiếu ẩn tới lớp bao, nó
tiết kiệm bộ nhớ và tránh rò rỉ ngoài ý muốn.
Lớp nội bộ, tức lớp thành viên không static, thì ngược lại: mỗi thể hiện của nó luôn gắn với
một thể hiện của lớp bao và đọc thẳng được trường thể hiện của lớp đó. Ta chọn nó khi lớp con
cần dùng chung trạng thái với đối tượng bao, ví dụ một trình lặp cần đọc dữ liệu bên trong tập
hợp mà nó thuộc về. Cái giá phải trả là thể hiện lớp nội bộ luôn níu giữ đối tượng bao, nên
nếu vòng đời hai bên lệch nhau thì dễ giữ bộ nhớ lâu hơn cần thiết.
Lớp cục bộ khai báo ngay bên trong một phương thức và chỉ nhìn thấy được trong khối chứa nó. Nó có tên, nên khác lớp ẩn danh ở chỗ ta tạo được nhiều thể hiện của nó và định nghĩa thêm hàm khởi tạo. Ta chọn nó khi cần một lớp trợ giúp cục bộ, dùng vài lần trong đúng một phương thức, và không muốn nó lộ ra ngoài.
Lớp ẩn danh là loại thứ tư, đứng ngang hàng ba loại trên chứ không phải một biến thể của lớp cục bộ. Nó không có tên, gộp khai báo với khởi tạo vào một biểu thức, và chỉ đẻ ra đúng một thể hiện. Ta chọn nó khi cần cài đặt nhanh một giao tiếp hay ghi đè một lớp cho đúng một lần dùng, thường là một hàm gọi lại truyền vào phương thức khác. Đặt cạnh lớp cục bộ, khác biệt là lớp cục bộ có tên nên tạo được nhiều thể hiện và có hàm khởi tạo riêng, còn lớp ẩn danh thì không.
Một cách sắp xếp lại bốn loại cho dễ nhớ là hỏi hai câu. Câu thứ nhất: lớp này khai báo ở đâu, ở cấp thành viên của lớp bao hay ở bên trong thân một phương thức? Hai loại đầu là thành viên, hai loại sau nằm trong phương thức. Câu thứ hai: lớp này có gắn với một thể hiện của lớp bao hay không? Lớp lồng tĩnh thì không, ba loại còn lại thì có, miễn là chúng được tạo ra trong ngữ cảnh có thể hiện. Hai câu hỏi ấy chia bốn loại thành một bảng gọn gàng, và quan trọng hơn, chúng chỉ thẳng vào cái tính chất gây hệ luỵ nhiều nhất, đó là tham chiếu ngầm tới đối tượng bao. Phần ngay dưới đây nói về nó.
Tham chiếu ngầm tới đối tượng bao
Đây là kiến thức mà rất nhiều người học xong bốn loại lớp lồng rồi vẫn không nắm, và nó là nguồn gốc của một lớp lỗi rất khó tìm.
Khi ta viết một lớp nội bộ, tức lớp thành viên không static, trình biên dịch âm thầm thêm
vào lớp ấy một trường ẩn trỏ về đối tượng bao, và âm thầm thêm một tham số vào hàm khởi tạo
để truyền đối tượng bao vào. Đó chính là cơ chế cho phép thân lớp nội bộ đọc thẳng trường
thể hiện của lớp bao mà không phải viết gì thêm: bên dưới, mọi truy cập ấy đều được dịch
thành lời gọi qua cái trường ẩn kia. Lớp cục bộ và lớp ẩn danh, khi được tạo ra trong một
ngữ cảnh có thể hiện, cũng mang theo đúng tham chiếu ngầm ấy. Chỉ có lớp lồng tĩnh là không,
vì nó không gắn với thể hiện nào cả.
Tham chiếu ngầm này tiện, nhưng nó có hai hệ luỵ mà ta phải biết.
Hệ luỵ thứ nhất là chuyện this. Trong thân một lớp nội bộ hay lớp ẩn danh, this trỏ về
chính thể hiện của lớp lồng, không trỏ về đối tượng bao. Muốn trỏ về đối tượng bao phải
viết TenLopBao.this. Nếu lớp lồng có một trường trùng tên với trường của lớp bao, trường
của lớp lồng sẽ che mất trường kia, và đây là chỗ sinh ra những lỗi đọc nhầm biến rất khó
phát hiện bằng mắt.
public class NguoiDung {
private String ten = "lop bao";
class The { // inner class, not static
private String ten = "lop noi bo";
void in() {
System.out.println(ten); // lop noi bo
System.out.println(this.ten); // lop noi bo
System.out.println(NguoiDung.this.ten); // lop bao
}
}
public static void main(String[] args) {
NguoiDung nd = new NguoiDung();
The t = nd.new The(); // note the syntax: outer.new Inner()
t.in();
}
}
Chú ý cả cú pháp khởi tạo nd.new The(). Nó trông lạ mắt nhưng rất logic: vì mọi thể hiện
lớp nội bộ đều phải gắn với một đối tượng bao, ta buộc phải nói rõ đối tượng bao là ai. Nếu
thử viết new The() từ một ngữ cảnh static như main, trình biên dịch sẽ báo lỗi vì
không có đối tượng bao nào để gắn vào. Chính thông báo lỗi ấy là bằng chứng cho thấy tham
chiếu ngầm có thật.
Hệ luỵ thứ hai nghiêm trọng hơn, đó là nguy cơ rò rỉ bộ nhớ. Bộ thu gom rác của Java chỉ dọn những đối tượng không còn ai tham chiếu tới. Một thể hiện lớp nội bộ luôn níu giữ đối tượng bao của nó, nên chừng nào thể hiện lớp nội bộ còn sống thì đối tượng bao cũng không thể bị dọn, dù chương trình không còn dùng đối tượng bao ấy nữa. Chuyện chỉ thành vấn đề khi vòng đời hai bên lệch nhau: ta tạo một đối tượng bao nặng, lấy từ nó ra một đối tượng lớp nội bộ nhỏ xíu, rồi cất đối tượng nhỏ ấy vào một chỗ sống lâu, chẳng hạn một danh sách bộ lắng nghe ở cấp toàn cục hay một bộ nhớ đệm tĩnh. Từ lúc đó, cái đối tượng bao nặng bị neo lại trong bộ nhớ vô thời hạn qua sợi dây mà ta không hề nhìn thấy trong mã nguồn.
import java.util.ArrayList;
import java.util.List;
public class RoRi {
// A long-lived registry: anything put in here stays alive for good
private static final List<Runnable> SO_TAY = new ArrayList<>();
static class Nang { // static: no hidden outer reference
private final byte[] duLieu = new byte[10_000_000]; // heavy payload
Runnable taoAnDanh() {
return new Runnable() { // anonymous class inside an instance
@Override public void run() {
System.out.println("chay"); // does not even touch duLieu
}
};
}
}
public static void main(String[] args) {
Nang n = new Nang();
SO_TAY.add(n.taoAnDanh()); // the anonymous object silently pins n alive
n = null; // we drop our reference, but n cannot be collected
System.out.println(SO_TAY.size());
}
}
Điểm cay đắng của ví dụ trên là lớp ẩn danh không hề đụng tới trường duLieu. Nó chỉ in
ra một chuỗi. Vậy mà mười megabyte kia vẫn không được dọn, chỉ vì tham chiếu ngầm luôn có
mặt, bất kể ta có dùng tới đối tượng bao hay không. Đọc mã bằng mắt thường thì không thấy
sợi dây ấy ở đâu cả, và đó chính là lý do loại rò rỉ này khó truy.
Cách chữa đơn giản đến bất ngờ: nếu lớp lồng không cần trạng thái của đối tượng bao, hãy
khai báo nó static. Một lớp lồng tĩnh không mang tham chiếu ngầm, nên không neo giữ ai cả.
Đây là lý do nhiều tài liệu khuyên một câu ngắn gọn: mặc định hãy để lớp lồng là static, và
chỉ bỏ static đi khi thật sự cần đọc trạng thái của đối tượng bao. Với lớp ẩn danh, cách
tương đương là đừng cho nó sống lâu hơn đối tượng bao, hoặc thay nó bằng một Lambda không bắt
biến nào, hoặc rút phần dữ liệu cần dùng ra một biến cục bộ rồi chỉ bắt biến ấy.
Đừng tưởng đổi lớp ẩn danh sang Lambda là hết rò rỉ. Một Lambda có bắt đối tượng bao nếu
thân nó tham chiếu tới this hoặc tới một trường thể hiện, kể cả khi ta viết tên trường trần
không kèm this. Khác biệt là Lambda chỉ bắt khi thật sự cần, còn lớp ẩn danh thì bắt vô điều
kiện. Muốn chắc chắn không neo giữ gì, hãy viết Lambda chỉ đọc biến cục bộ và tham số.
Lớp ẩn danh làm bộ lắng nghe
Kiểu dùng phổ biến nhất của lớp ẩn danh là làm hàm gọi lại, hay còn gọi là bộ lắng nghe. Một phương thức nhận vào một đối tượng cài đặt giao tiếp, rồi tự gọi phương thức của đối tượng đó khi có sự kiện. Ta truyền một lớp ẩn danh vào đúng chỗ tham số.
interface SuKien { // secondary type: not public, one file
void xuLy(String thongDiep); // called back when an event happens
}
public class NutBam {
private SuKien listener;
public void datListener(SuKien s) { this.listener = s; }
public void nhan() { // simulate a click
if (listener != null) listener.xuLy("da nhan nut");
}
public static void main(String[] args) {
NutBam nut = new NutBam();
nut.datListener(new SuKien() { // anonymous listener
@Override public void xuLy(String thongDiep) {
System.out.println("Phan hoi: " + thongDiep);
}
});
nut.nhan(); // prints "Phan hoi: da nhan nut"
}
}
Điểm hay ở đây là hành động phản hồi nằm ngay cạnh nút bấm, đọc mã tới đâu hiểu tới đó, không phải nhảy sang một tệp khác. Nếu sau này cần một nút khác với phản hồi khác, ta chỉ việc truyền một lớp ẩn danh khác, thân lớp cũ không bị đụng tới.
Lớp ẩn danh làm bộ so sánh
Một kiểu dùng kinh điển khác là truyền một bộ so sánh cho phương thức sắp xếp. Giao tiếp
Comparator có một phương thức compare quyết định thứ tự hai phần tử. Ta thường cần một
thứ tự riêng cho đúng một lần sắp, nên viết hẳn một lớp Comparator là thừa.
import java.util.Arrays;
import java.util.Comparator;
public class SapXep {
public static void main(String[] args) {
String[] ten = {"Binh", "An", "Cuong"};
Arrays.sort(ten, new Comparator<String>() { // anonymous comparator
@Override public int compare(String a, String b) {
return b.length() - a.length(); // longer names first
}
});
System.out.println(Arrays.toString(ten)); // [Cuong, Binh, An]
}
}
Bộ so sánh trên xếp chuỗi dài đứng trước. Vì thứ tự này chỉ dùng đúng ở đây, lớp ẩn danh là lựa chọn gọn nhất. Khi cần một thứ tự khác cho lần sắp khác, ta truyền một lớp ẩn danh mới.
Lớp ẩn danh so với Lambda
Cả hai ví dụ trên đều cài giao tiếp hàm, tức giao tiếp chỉ có một phương thức trừu tượng. Với loại giao tiếp này, biểu thức Lambda viết gọn hơn hẳn vì bỏ được phần khung lặp lại.
// anonymous class form
Arrays.sort(ten, new Comparator<String>() {
@Override public int compare(String a, String b) {
return b.length() - a.length();
}
});
// lambda form, same behaviour
Arrays.sort(ten, (a, b) -> b.length() - a.length());
Tuy vậy lớp ẩn danh không hề lỗi thời. Lambda chỉ dùng được cho giao tiếp hàm một phương thức;
khi cần cài một giao tiếp có nhiều phương thức, hoặc cần ghi đè một lớp và giữ trạng thái
riêng, hoặc cần dùng this trỏ về chính thể hiện lớp ẩn danh, ta vẫn phải viết lớp ẩn danh.
Một khác biệt tinh tế nữa: bên trong lớp ẩn danh, từ khóa this trỏ về thể hiện lớp ẩn danh,
còn trong Lambda thì this trỏ về thể hiện lớp bao ngoài.
Ba khác biệt thật sự, ngoài chuyện dài ngắn
Nếu chỉ nhìn vào số dòng thì Lambda thắng tuyệt đối, và người học dễ kết luận sai rằng Lambda là "cách viết tắt" của lớp ẩn danh. Hai thứ này khác nhau ở bản chất chứ không chỉ ở hình thức, và có ba khác biệt đáng nhớ.
Khác biệt thứ nhất nằm ở chỗ Lambda không sinh ra một lớp mới. Mỗi lớp ẩn danh ta viết
đều được trình biên dịch dịch thành một tệp lớp riêng, mang cái tên xấu xí kiểu
NutBam$1.class. Viết mười lớp ẩn danh là có thêm mười tệp lớp phải nạp lúc chạy. Lambda thì
không: trình biên dịch để lại một chỉ dẫn đặc biệt, và máy ảo dựng cài đặt cho nó ngay lúc
chạy, lần đầu tiên chỗ ấy được dùng tới. Hệ quả thực tế là chương trình dùng nhiều Lambda
khởi động nhẹ hơn và tốn ít bộ nhớ siêu dữ liệu hơn so với chương trình dùng nhiều lớp ẩn
danh làm cùng một việc.
Khác biệt thứ hai là this, và đây là chỗ hay làm người ta lúng túng nhất. Lớp ẩn danh
là một lớp thật, nên nó có phạm vi riêng, và this bên trong nó trỏ về chính nó. Lambda thì
không tạo phạm vi mới, nó nằm hẳn trong phạm vi của phương thức bao quanh, nên this bên
trong Lambda vẫn là this của lớp bao. Điều này có mặt tốt: trong Lambda, muốn gọi một
phương thức của lớp bao thì cứ gọi thẳng, không cần cú pháp TenLopBao.this.
public class SoSanhThis {
private String ten = "lop bao";
public void chay() {
Runnable anDanh = new Runnable() {
@Override public void run() {
// this refers to the anonymous object, so we must qualify to reach outer
System.out.println(SoSanhThis.this.ten); // lop bao
System.out.println(this.getClass().getSimpleName()); // an empty name
}
};
Runnable lam = () -> {
// this is the enclosing SoSanhThis instance: no qualification needed
System.out.println(this.ten); // lop bao
System.out.println(this.getClass().getSimpleName()); // SoSanhThis
};
anDanh.run();
lam.run();
}
public static void main(String[] args) { new SoSanhThis().chay(); }
}
Khác biệt thứ ba là phạm vi tên. Vì lớp ẩn danh mở ra một phạm vi mới, nó được phép khai báo một biến trùng tên với biến cục bộ bên ngoài, và biến bên trong sẽ che biến bên ngoài. Lambda không mở phạm vi mới, nên khai báo một tham số hay biến trùng tên với biến cục bộ bao quanh là lỗi biên dịch ngay. Nghe thì có vẻ Lambda khắt khe hơn, nhưng thực chất đây là một ưu điểm: nó chặn đứng loại lỗi che biến vốn rất khó nhìn ra bằng mắt.
Tổng kết cách chọn cho gọn. Cứ mặc định dùng Lambda khi mục tiêu là một giao tiếp hàm, tức
giao tiếp chỉ có một phương thức trừu tượng, vì Lambda ngắn hơn, nhẹ hơn và ít bẫy hơn. Chỉ
quay lại lớp ẩn danh trong ba tình huống: giao tiếp có nhiều hơn một phương thức trừu tượng;
ta cần kế thừa và ghi đè một lớp chứ không phải cài một giao tiếp; hoặc ta cần đối tượng
ấy giữ trạng thái riêng và cần this trỏ về chính nó.
Truy cập biến bao quanh
Lớp ẩn danh bắt được cả trường của lớp bao lẫn biến cục bộ của phương thức chứa nó. Với biến
cục bộ, Java bắt buộc biến phải là final hoặc thực chất không đổi, nghĩa là gán một lần rồi
không đổi nữa. Lý do là lớp ẩn danh có thể sống lâu hơn phương thức đã tạo ra nó, nên nó giữ
một bản sao giá trị biến; nếu cho phép biến gốc đổi thì bản sao sẽ lệch, gây rối loạn khó lần.
public class BienBao {
private String tienTo = "Log: "; // instance field, always accessible
public Runnable tao() {
int lan = 3; // effectively final local variable
return new Runnable() {
@Override public void run() {
System.out.println(tienTo + "chay lan thu " + lan);
}
};
}
}
Ở đây tienTo là trường thể hiện nên lớp ẩn danh đọc trực tiếp và không đòi hỏi gì thêm. Còn
lan là biến cục bộ, ta không gán lại nó sau khi khởi tạo nên nó thực chất không đổi và được
phép dùng. Nếu thêm một dòng lan = 4; ở dưới, trình biên dịch lập tức báo lỗi.
Vì sao Java bắt buộc như vậy
Quy tắc "gần như bất biến" nghe tuỳ tiện cho tới khi ta hiểu biến cục bộ sống ở đâu. Biến cục
bộ nằm trên ngăn xếp của lời gọi phương thức, và ngăn xếp ấy bị xoá sạch ngay khi phương
thức trả về. Trong khi đó, đối tượng lớp ẩn danh nằm trên bộ nhớ động và hoàn toàn có thể
sống lâu hơn phương thức đã sinh ra nó, đúng như trong ví dụ trên: phương thức tao() trả về
đối tượng Runnable rồi kết thúc, nhưng đối tượng ấy còn được cất đi và chạy về sau. Nếu lớp
ẩn danh giữ một con trỏ tới ô nhớ của biến lan trên ngăn xếp, thì tới lúc nó chạy, ô nhớ ấy
đã bị thu hồi và ghi đè bởi lời gọi khác.
Java giải quyết bằng cách sao chép giá trị. Lúc tạo đối tượng lớp ẩn danh, trình biên dịch âm thầm sao giá trị của mọi biến cục bộ được bắt vào các trường ẩn của đối tượng ấy. Từ đó về sau, lớp ẩn danh làm việc với bản sao của mình chứ không đọc ô nhớ gốc nữa. Kiểu bắt biến này gọi là bắt theo giá trị.
Tới đây thì quy tắc trở nên hiển nhiên. Nếu cho phép gán lại biến gốc, ta lập tức có hai
nguồn sự thật cho cùng một cái tên: bản gốc trên ngăn xếp và bản sao trong đối tượng. Sửa bản
này thì bản kia không hay biết, và người đọc mã sẽ không đoán nổi giá trị nào sẽ được in ra.
Java đóng cửa vấn đề bằng cách cấm luôn: đã bắt thì biến phải là final hoặc thực chất không
đổi, tức là gán một lần rồi thôi. Khi hai bản sao không bao giờ đổi thì chúng không bao giờ
lệch nhau, và câu hỏi "giá trị nào mới đúng" không còn tồn tại.
Chú ý một chỗ tinh vi: quy tắc này chỉ áp cho biến cục bộ, không áp cho trường thể
hiện. Trường thể hiện nằm trên bộ nhớ động cùng với đối tượng bao, và lớp ẩn danh truy cập
nó qua tham chiếu ngầm chứ không sao chép, nên nó đọc luôn giá trị mới nhất. Điều này giải
thích một mẹo mà ta hay thấy trong mã thực tế: khi cần một biến đếm thay đổi được bên trong
lớp ẩn danh, người ta không dùng biến cục bộ int mà bọc nó vào một đối tượng, ví dụ một
mảng một phần tử hoặc một AtomicInteger. Bản thân biến tham chiếu vẫn thực chất không đổi
nên hợp lệ, còn cái bị sửa là nội dung của đối tượng nó trỏ tới.
import java.util.concurrent.atomic.AtomicInteger;
public class DemLan {
public static void main(String[] args) {
AtomicInteger dem = new AtomicInteger(); // the reference never changes
Runnable r = new Runnable() {
@Override public void run() {
dem.incrementAndGet(); // we mutate the object, not the variable
}
};
r.run();
r.run();
System.out.println(dem.get()); // 2
}
}
Mẹo này hợp lệ nhưng nên dùng dè dặt. Nó lách được luật chứ không xoá được lý do đẻ ra luật, và một biến đếm bị nhiều đoạn mã cùng sửa vẫn khó lần như thường, nhất là khi có nhiều luồng. Khi nào thấy mình phải viết như vậy, hãy dừng lại một nhịp và tự hỏi liệu có cách nào diễn đạt cùng ý định mà không cần trạng thái sửa được hay không, ví dụ trả về giá trị thay vì tích luỹ vào một biến bên ngoài.
Gán lại biến cục bộ sau khi lớp ẩn danh đã tham chiếu tới nó sẽ khiến biên dịch hỏng với thông
báo biến phải là final hoặc thực chất không đổi. Một lỗi khác là quên từ khóa new trước tên
giao tiếp, khiến trình biên dịch không hiểu đây là lớp ẩn danh. Ngoài ra, đừng nhầm this
trong lớp ẩn danh với this của lớp bao; muốn trỏ về lớp bao phải viết TenLopBao.this.
Bài tập thực hành
Bài tập 1: lớp ẩn danh cài đặt XuLy
Dùng lớp ẩn danh tạo một thể hiện của giao tiếp XuLy (một phương thức chay) in ra một
thông báo, rồi gọi nó. Sau đó nêu nhận xét: vì sao khối lớp ẩn danh này có thể viết gọn thành
Lambda ở chương 2.
Lời giải chi tiết
Bước 1: khai báo giao tiếp XuLy chỉ một phương thức. Bước 2: dùng new XuLy() { ... } để
vừa khai báo vừa tạo thể hiện trong một biểu thức, không cần tệp lớp riêng. Bước 3: gọi
phương thức chay() trên thể hiện vừa tạo.
interface XuLy { // secondary type: not public, one file
void chay(); // single abstract method
}
public class Demo {
public static void main(String[] args) {
XuLy viec = new XuLy() { // anonymous class, soon a lambda
@Override public void chay() {
System.out.println("Dang chay mot tac vu");
}
};
viec.chay();
}
}
Nhận xét: vì XuLy chỉ có một phương thức trừu tượng nên nó là giao tiếp hàm. Cả khối lớp
ẩn danh trên về sau viết gọn thành XuLy viec = () -> System.out.println("Dang chay mot tac vu");, đó chính là biểu thức Lambda của chương 2.
Bài tập 2: lớp ẩn danh đọc biến bao quanh
Viết main khai báo một biến cục bộ String chứa tên một công việc, rồi tạo một lớp ẩn danh
cài đặt XuLy in ra tên công việc đó. Lưu ý biến cục bộ phải thực chất không đổi.
Lời giải chi tiết
Bước 1: khai báo biến cục bộ và không gán lại nó về sau, nhờ vậy nó thực chất không đổi. Bước
2: lớp ẩn danh đọc biến bao quanh ngay trong thân chay(). Bước 3: gọi chay() để in.
interface XuLy { // secondary type: not public, one file
void chay();
}
public class Demo {
public static void main(String[] args) {
String tenViec = "Sao luu du lieu"; // effectively final, never reassigned
XuLy viec = new XuLy() {
@Override public void chay() {
System.out.println("Dang chay: " + tenViec); // reads outer variable
}
};
viec.chay();
}
}
Ý chính: lớp ẩn danh bắt được biến cục bộ bao quanh, nhưng chỉ khi biến ấy là final hoặc
thực chất không đổi; nếu gán lại tenViec sau đó, trình biên dịch sẽ báo lỗi.
Bài tập 3: đổi lớp ẩn danh sang Lambda và chỉ ra chỗ không đổi được
Cho một mảng tên. Hãy sắp xếp nó theo độ dài tăng dần, tên nào cùng độ dài thì xếp theo thứ
tự bảng chữ cái, bằng một Comparator viết dưới dạng lớp ẩn danh. Sau đó viết lại bằng
Lambda. Cuối cùng trả lời: nếu bên trong bộ so sánh ta cần in ra tên lớp của chính bộ so sánh
ấy qua this.getClass(), thì hai cách viết cho kết quả khác nhau ra sao, và vì sao?
Lời giải chi tiết
Bước 1, viết bằng lớp ẩn danh. Phép so sánh gồm hai tầng: so độ dài trước, nếu bằng nhau mới
so nội dung chuỗi. Chú ý dùng Integer.compare chứ không dùng phép trừ, vì phép trừ có thể
tràn số khi hai giá trị quá xa nhau.
import java.util.Arrays;
import java.util.Comparator;
public class SapTheoDoDai {
public static void main(String[] args) {
String[] ten = {"Cuong", "An", "Binh", "Ha"};
Arrays.sort(ten, new Comparator<String>() { // anonymous class form
@Override public int compare(String a, String b) {
int d = Integer.compare(a.length(), b.length()); // never a - b
return d != 0 ? d : a.compareTo(b); // tie break
}
});
System.out.println(Arrays.toString(ten)); // [An, Ha, Binh, Cuong]
}
}
Bước 2, viết lại bằng Lambda. Vì Comparator là giao tiếp hàm nên Lambda thay thế được trọn
vẹn, và thư viện còn cho ta cách viết khai báo hơn nữa qua comparingInt và thenComparing.
import java.util.Arrays;
import java.util.Comparator;
public class SapTheoDoDaiLam {
public static void main(String[] args) {
String[] ten = {"Cuong", "An", "Binh", "Ha"};
Arrays.sort(ten, (a, b) -> { // plain lambda form
int d = Integer.compare(a.length(), b.length());
return d != 0 ? d : a.compareTo(b);
});
// Same order, expressed declaratively
Arrays.sort(ten, Comparator.comparingInt(String::length)
.thenComparing(Comparator.naturalOrder()));
System.out.println(Arrays.toString(ten)); // [An, Ha, Binh, Cuong]
}
}
Bước 3, câu hỏi về this. Hai cách viết cho kết quả khác hẳn nhau. Trong lớp ẩn danh,
this trỏ về chính thể hiện lớp ẩn danh, nên this.getClass() cho ra lớp do trình biên dịch
sinh ra, tên kiểu SapTheoDoDai$1. Trong Lambda, this trỏ về thể hiện của lớp bao ngoài,
vì Lambda không tạo phạm vi mới và cũng không sinh ra một lớp mới, nên this.getClass() cho
ra chính lớp bao.
Còn một điểm phải nói rõ: đoạn Lambda ở trên nằm trong main, mà main là static nên
không hề có this nào cả. Viết this trong một Lambda đặt ở ngữ cảnh static là lỗi biên
dịch, trong khi lớp ẩn danh vẫn có this của riêng nó và biên dịch bình thường. Đây chính là
minh chứng rõ nhất cho luận điểm ở phần so sánh: lớp ẩn danh là một lớp thật với danh tính
riêng, còn Lambda chỉ là một mẩu hành vi sống nhờ trong phạm vi bao quanh nó.
Câu hỏi tự kiểm
- 1Bên trong một lớp ẩn danh, từ khóa this trỏ tới đâu?
- 2Vì sao biến cục bộ bị lớp ẩn danh bắt phải là final hoặc thực chất không đổi?
- 3Khác biệt nào sau đây ĐÚNG giữa Lambda và lớp ẩn danh?
- 4Một lớp nội bộ không static, hoặc lớp ẩn danh tạo trong ngữ cảnh thể hiện, gây rò rỉ bộ nhớ khi nào?
Tóm tắt
- Bốn loại lớp lồng là lớp lồng tĩnh, lớp nội bộ, lớp cục bộ và lớp ẩn danh. Chúng ngang hàng nhau, phân biệt theo hai câu hỏi: khai báo ở cấp thành viên hay trong thân phương thức, và có gắn với một thể hiện của lớp bao hay không.
- Ba loại không
staticđều mang một tham chiếu ngầm tới đối tượng bao. Tham chiếu ấy cho phép đọc trường của lớp bao, nhưng cũng neo giữ đối tượng bao trong bộ nhớ, gây rò rỉ khi vòng đời hai bên lệch nhau. - Quy tắc mặc định: lớp lồng nên
static, chỉ bỏstatickhi thật sự cần trạng thái của đối tượng bao. - Lớp ẩn danh gộp khai báo và khởi tạo vào một biểu thức, hợp khi chỉ cần đúng một thể hiện, điển hình là bộ lắng nghe và bộ so sánh.
- Lambda không sinh ra một lớp mới, không mở phạm vi mới, và
thistrong Lambda trỏ về lớp bao chứ không trỏ về chính nó. Với giao tiếp hàm, hãy mặc định dùng Lambda. - Lớp ẩn danh vẫn cần thiết khi giao tiếp có nhiều phương thức trừu tượng, khi phải ghi đè
một lớp, hoặc khi cần
thistrỏ về chính đối tượng ấy. - Biến cục bộ được bắt phải là
finalhoặc thực chất không đổi, vì nó được sao chép theo giá trị; trường thể hiện thì không chịu ràng buộc này vì được truy cập qua tham chiếu ngầm.
Lớp ẩn danh gộp khai báo và khởi tạo vào một biểu thức, hợp khi chỉ cần đúng một thể hiện. Với giao tiếp hàm, lớp ẩn danh là bậc thang dẫn vào Lambda ở chương 2.