Backend

99 Ngày Java — Ngày 19: Inner class & anonymous class

SSite Admin
15 tháng 08, 2026 6 phút đọc 0 lượt xem
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.

Sketchnote Ngày 19: inner class & anonymous class — member/static nested, local class, capture biến effectively final, và bảng chọn dạng phù hợp

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ị.

Màn hình laptop hiển thị mã nguồn — nested class giữ các cài đặt nhỏ đúng nơi chúng được sử dụng

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.Entry là static nested vì độc lập với trạng thái Map; iterator của ArrayList là inner class vì phải đọc mảng nội bộ của list.

Bài tập nhỏ

  • Viết DonHang với inner class ChiTiet và static nested TrangThai — so sánh cú pháp khởi tạo hai loại và giải thích khác biệt.

  • Đổi ChiTiet thành static — quan sát lỗi biên dịch tại chỗ đọc maDon và 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 BoLocNhanVien từ 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!

S

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.

Bài viết liên quan

99 Ngày Spring — Ngày 19: Upload & download file

Ngày 19 của 99 Ngày Spring: upload với MultipartFile và tên lưu trữ do server sinh, giới hạn max-file-size/max-request-size với lỗi 413 chuẩn ProblemDetail, download stream qua Resource với Content-Disposition, chặn path traversal bằng normalize + startsWith, và checklist an toàn cho production.

15 thg 8, 20265 phút0
99 Ngày Java — Ngày 18: static & final

Ngày 18 của 99 Ngày Java: static — thành viên thuộc về lớp thay vì đối tượng, static block và static import, ba cấp độ của final (biến, method, lớp), mẫu hằng số public static final, và ranh giới quan trọng: static bất biến thì an toàn, static có thể ghi là trạng thái toàn cục nguy hiểm.

14 thg 8, 20266 phút46
99 Ngày Spring — Ngày 18: Phân trang & sắp xếp API

Ngày 18 của 99 Ngày Spring: vì sao không trả cả bảng trong một response, Pageable tự bind page/size/sort từ query param, bọc Page thành PageResponse DTO để giữ hợp đồng ổn định, convention sắp xếp nhiều tiêu chí, và hai chốt an toàn — giới hạn max-page-size, whitelist field được sort.

14 thg 8, 20265 phút54