99 Ngày Java — Ngày 19: Inner class & anonymous class
Không phải lớp nào cũng xứng đáng có một file riêng. Một cấu trúc chỉ phục vụ DonHang, một bộ lọc chỉ dùng trong đúng một method, một cài đặt interface chỉ cần đúng một lần — Java cho phép khai báo lớp bên trong lớp khác, thậm chí bên trong method hoặc ngay trong một biểu thức. Hôm nay ta phân loại bốn dạng nested class: member inner, static nested, local và anonymous — cùng quy tắc chọn dạng nào cho tình huống nào.

Member inner class & static nested class
public class DonHang {
private String maDon;
public class ChiTiet { // inner class (member): gắn với
void in() { // MỘT đối tượng DonHang cụ thể
System.out.println(maDon); // → đọc trực tiếp field của lớp ngoài
}
}
public static class TrangThai { } // static nested: KHÔNG giữ
} // tham chiếu tới đối tượng ngoài
DonHang don = new DonHang("DH01");
DonHang.ChiTiet ct = don.new ChiTiet(); // inner: cần đối tượng ngoài
DonHang.TrangThai tt = new DonHang.TrangThai(); // static nested: độc lập
// Inner class giữ một tham chiếu NGẦM tới đối tượng ngoài — có thể khiến
// đối tượng ngoài không được giải phóng ngoài ý muốn. Nguyên tắc
// (Effective Java, Item 24): mặc định dùng STATIC nested; chỉ bỏ static
// khi thật sự cần truy cập trạng thái của lớp ngoài.Khác biệt cốt lõi nằm ở tham chiếu ngầm: inner class luôn mang theo một tham chiếu tới đối tượng ngoài (nên mới đọc được
maDon); static nested thì không — nó chỉ là một lớp bình thường được đặt tên bên trong lớp khác để gom nhóm.Tham chiếu ngầm đó có giá: đối tượng ngoài không thể được giải phóng chừng nào inner object còn sống — một nguồn giữ bộ nhớ kín đáo (chủ đề memory leak: Ngày 80).
Quy tắc thực dụng từ Effective Java: mặc định static nested; chỉ chuyển sang inner khi thật sự cần chạm vào trạng thái của lớp ngoài.
Local class & quy tắc capture
public List<NhanVien> locTheoLuong(double nguong) {
class LocLuong implements BoLocNhanVien { // local class: khai báo TRONG method
@Override
public boolean kiemTra(NhanVien nv) {
return nv.tinhLuong() > nguong; // "capture" biến của method bao ngoài
}
}
return loc(new LocLuong());
}
// Biến được capture phải EFFECTIVELY FINAL —
// không bị gán lại sau lần khởi tạo đầu tiên:
double nguong = 20_000_000;
// nguong = 25_000_000; // ✗ chỉ cần dòng này tồn tại,
// mọi chỗ capture "nguong" đều lỗi biên dịch
// Lý do: giá trị biến được SAO CHÉP vào đối tượng lúc khởi tạo —
// nếu biến gốc còn thay đổi được, hai bên sẽ nhìn thấy hai giá trị khác nhau.
// Quy tắc này áp dụng nguyên vẹn cho lambda (Ngày 51).Local class sống trong đúng một method — phù hợp khi cài đặt cần tên (dùng lại vài lần trong method) hoặc nhiều method, nhưng không đáng xuất hiện ngoài phạm vi đó.
Quy tắc effectively final đáng hiểu sâu một lần: giá trị biến được sao chép vào đối tượng lúc khởi tạo, nên compiler cấm mọi khả năng biến gốc thay đổi về sau — tránh tình huống hai nơi nhìn thấy hai giá trị khác nhau.
Hiểu quy tắc này hôm nay là trả trước cho Ngày 51: lambda capture biến theo đúng cơ chế này, không khác một chi tiết nào.
Anonymous class — và ranh giới với lambda
// Anonymous class — khai báo + khởi tạo trong MỘT biểu thức (quen từ Ngày 17):
BoLocNhanVien loc = new BoLocNhanVien() {
@Override
public boolean kiemTra(NhanVien nv) {
return nv.tinhLuong() > 20_000_000;
}
};
// Với FUNCTIONAL interface (đúng 1 abstract method) — lambda thay thế gọn hơn:
BoLocNhanVien loc2 = nv -> nv.tinhLuong() > 20_000_000;
// Anonymous class vẫn là công cụ đúng khi:
// 1. Interface có NHIỀU abstract method cần cài đặt
// 2. Cần KẾ THỪA một class (lambda chỉ thay được functional interface)
// 3. Cài đặt cần FIELD — trạng thái riêng của nóAnonymous class là cài đặt dùng-một-lần: khai báo, override và khởi tạo gói trong một biểu thức — bạn đã dùng nó ở bài interface (Ngày 17) trước khi biết tên gọi chính thức.
Từ Java 8, phần lớn anonymous class nhường chỗ cho lambda — nhưng chỉ với functional interface; ba trường hợp trong code trên vẫn thuộc về anonymous class.
Anonymous class cũng tuân thủ quy tắc capture effectively final như local class — cùng một cơ chế sao chép giá trị.
Bảng quyết định — chọn dạng nào
// Chọn dạng nào — theo phạm vi sử dụng và nhu cầu trạng thái:
//
// static nested dùng ở nhiều nơi, KHÔNG cần trạng thái lớp ngoài ← mặc định
// inner (member) dùng ở nhiều nơi, CẦN trạng thái lớp ngoài
// local class chỉ dùng trong MỘT method, cần tên hoặc nhiều method
// anonymous dùng MỘT lần — interface nhiều method / extends class
// lambda dùng MỘT lần — functional interface (Ngày 51)
//
// Trong JDK:
// Map.Entry → static nested (không cần trạng thái của Map)
// Iterator của ArrayList → inner class (cần đọc mảng bên trong list)Hai trục quyết định: phạm vi sử dụng (nhiều nơi / một method / một lần) và nhu cầu trạng thái (có/không cần trạng thái lớp ngoài, có/không cần field riêng).
JDK minh họa cả hai đầu:
Map.Entrylà static nested vì độc lập với trạng thái Map; iterator củaArrayListlà inner class vì phải đọc mảng nội bộ của list.
Bài tập nhỏ
Viết
DonHangvới inner classChiTietvà static nestedTrangThai— so sánh cú pháp khởi tạo hai loại và giải thích khác biệt.Đổi
ChiTietthànhstatic— quan sát lỗi biên dịch tại chỗ đọcmaDonvà rút ra kết luận về tham chiếu ngầm.Viết local class capture một biến, rồi thêm một dòng gán lại biến đó — đọc kỹ thông báo lỗi "effectively final".
Lấy anonymous class
BoLocNhanVientừ Ngày 17, viết lại thành lambda — sau đó thêm một field đếm số lần gọi vào phiên bản anonymous và giải thích vì sao lambda không làm được điều này.
Kết luận
Bốn dạng nested class phục vụ một mục tiêu chung: đặt cài đặt đúng phạm vi nó được dùng — static nested làm mặc định, inner khi cần trạng thái lớp ngoài, local trong một method, anonymous cho lần dùng duy nhất, và quy tắc capture effectively final xuyên suốt tất cả (kể cả lambda sau này). Giai đoạn OOP sắp khép lại: Ngày 20 ta gặp record — cú pháp Java hiện đại cho lớp dữ liệu bất biến, thay hàng chục dòng boilerplate bằng một dòng khai báo. Hẹn gặp lại!
Site Admin
Engineer and writer. Building things with TypeScript and distributed systems.
Bình luận (0)
Bạn cần đăng nhập bằng Google để bình luận.
Hãy là người bình luận đầu tiên.


