Tìm hiểu về Lớp nội trong Java

Lớp nội (Inner Class) trong Java là một khái niệm mạnh mẽ cho phép định nghĩa một lớp bên trong một lớp khác. Mặc dù trong quá trình phát triển thông thường, việc tự viết lớp nội có thể không diễn ra thường xuyên, nhưng hiểu biết về chúng là rất quan trọng để đọc và làm việc với các thư viện phức tạp, cũng như để xây dựng các cấu trúc chương trình linh hoạt hơn.

Về cơ bản, lớp nội là một kiểu thành viên (member) của một lớp bên ngoài (Outer Class), giống như các trường (field) và phương thức (method). Không có giới hạn về cấu trúc lồng nhau, cho phép các lớp được định nghĩa sâu bên trong nhau. Dưới đây là ví dụ cơ bản về cách định nghĩa và sử dụng một lớp nội:

Cấu trúc cơ bản của Lớp nội

Hãy xem xét một ví dụ đơn giản về lớp nội không tĩnh:

class KhoangChua { // Lớp bên ngoài
    private String thongDiep = "Chào Thế Giới!";

    class ThanhPhanCon { // Lớp nội
        public void hienThi() {
            System.out.println(KhoangChua.this.thongDiep); // Truy cập thuộc tính của lớp bên ngoài
        }
    }

    public void kichHoatHienThi() {
        ThanhPhanCon phanCon = new ThanhPhanCon(); // Khởi tạo đối tượng lớp nội
        phanCon.hienThi(); // Gọi phương thức của lớp nội
    }
}

public class ChuongTrinhLopNoi {
    public static void main(String[] args) {
        KhoangChua khoangChua = new KhoangChua();
        khoangChua.kichHoatHienThi();
    }
}

Kết quả:

Chào Thế Giới!

Ví dụ trên cho thấy một lớp ThanhPhanCon được định nghĩa bên trong KhoangChua. Lớp nội có thể truy cập trực tiếp các thành viên (kể cả private) của lớp bên ngoài thông qua cú pháp OuterClass.this.member.

Ưu điểm của Lớp nội: Truy cập trực tiếp các thành viên riêng tư

Một trong những lợi ích lớn nhất của lớp nội là khả năng truy cập trực tiếp các thành viên riêng tư của lớp bên ngoài. Để thấy rõ điều này, hãy so sánh với trường hợp hai lớp được định nghĩa độc lập:

class NguonDuLieu { // Lớp độc lập
    private String thongTinRieng = "Dữ liệu mật!";

    public String layThongTinRieng() { // Phương thức getter để truy cập dữ liệu riêng tư
        return thongTinRieng;
    }

    public void xuLyThongTin() {
        // Cần truyền đối tượng NguonDuLieu cho LopXuLy nếu muốn truy cập dữ liệu của nó
        LopXuLy xuLy = new LopXuLy(this);
        xuLy.inThongTin();
    }
}

class LopXuLy { // Lớp độc lập
    private NguonDuLieu nguon;

    public LopXuLy(NguonDuLieu nguon) {
        this.nguon = nguon;
    }

    public void inThongTin() {
        // Phải thông qua getter của NguonDuLieu để truy cập thuộc tính riêng tư
        System.out.println("Nội dung từ lớp ngoài: " + this.nguon.layThongTinRieng());
    }
}

public class ChuongTrinhSoSanhLop {
    public static void main(String[] args) {
        NguonDuLieu nguon = new NguonDuLieu();
        nguon.xuLyThongTin();
    }
}

Kết quả:

Nội dung từ lớp ngoài: Dữ liệu mật!

Như bạn thấy, khi LopXuLy là một lớp độc lập, nó cần một tham chiếu đến NguonDuLieu và phải sử dụng phương thức getter để truy cập dữ liệu riêng tư. Ngược lại, lớp nội có thể truy cập trực tiếp các trường private của lớp bên ngoài mà không cần thông qua getter, giảm thiểu boilerplate code và cải thiện tính đóng gói logic.

Không chỉ lớp nội có thể truy cập thành viên riêng tư của lớp bên ngoài, mà lớp bên ngoài cũng có thể truy cập thành viên riêng tư của lớp nội:

class NguoiQuanLy { // Lớp bên ngoài
    private String duLieuHeThong = "Thông tin cơ bản";

    class ThanhVienRieng { // Lớp nội
        private String thongBaoRieng = "Chỉ người quản lý biết";

        public void xemDuLieuHeThong() {
            System.out.println("Dữ liệu hệ thống: " + NguoiQuanLy.this.duLieuHeThong);
        }
    }

    public void xemThongBaoNoiBo() {
        ThanhVienRieng thanhVien = new ThanhVienRieng(); // Khởi tạo lớp nội
        thanhVien.xemDuLieuHeThong(); // Gọi phương thức của lớp nội
        System.out.println("Thông báo riêng từ thành viên: " + thanhVien.thongBaoRieng); // Truy cập thuộc tính private của lớp nội
    }
}

public class TruyCapLopNoi {
    public static void main(String[] args) {
        NguoiQuanLy quanLy = new NguoiQuanLy();
        quanLy.xemThongBaoNoiBo();
    }
}

Kết quả:

Dữ liệu hệ thống: Thông tin cơ bản
Thông báo riêng từ thành viên: Chỉ người quản lý biết

Khởi tạo Lớp nội từ bên ngoài

Thông thường, việc khởi tạo đối tượng lớp nội diễn ra bên trong lớp bên ngoài. Tuy nhiên, bạn cũng có thể khởi tạo một lớp nội từ bên ngoài lớp bên ngoài, miễn là lớp nội đó không có quyền truy cập private hoặc protected.

Cú pháp để khởi tạo một lớp nội từ bên ngoài như sau:

OuterClass.InnerClass innerObject = new OuterClass().new InnerClass();

Tên đầy đủ của một lớp nội trong ngữ cảnh Java là OuterClass.InnerClass. Trong các tệp bytecode, ký hiệu . sẽ được thay thế bằng $ (ví dụ: OuterClass$InnerClass.class).

class VungChinh { // Lớp bên ngoài
    private String noiDungBiMat = "Tài liệu tuyệt mật.";

    class VungPhu { // Lớp nội (không phải private)
        public void docNoiDung() {
            System.out.println("Đọc: " + VungChinh.this.noiDungBiMat);
        }
    }
}

public class KhoiTaoNgoaiLop {
    public static void main(String[] args) {
        // Khởi tạo đối tượng lớp nội từ bên ngoài
        VungChinh.VungPhu phu = new VungChinh().new VungPhu();
        phu.docNoiDung();
    }
}

Kết quả:

Đọc: Tài liệu tuyệt mật.

Modifier truy cập cho Lớp nội

Lớp nội có thể sử dụng các modifier truy cập như private, protected, public hoặc package-private. Khi một lớp nội được khai báo là private, nó chỉ có thể được truy cập bên trong lớp bên ngoài của nó. Cố gắng khởi tạo nó từ bên ngoài sẽ gây ra lỗi biên dịch.

class DoiTuongCha { // Lớp bên ngoài
    private String thongTinRiengTu = "Không ai biết trừ tôi!";

    private class DoiTuongConRiengTu { // Lớp nội private
        public void chiaSeThongTin() {
            System.out.println("Thông tin riêng tư: " + DoiTuongCha.this.thongTinRiengTu);
        }
    }

    public void truyCapNoiBo() {
        DoiTuongConRiengTu con = new DoiTuongConRiengTu();
        con.chiaSeThongTin();
    }
}

public class LopNoiRiengTu {
    public static void main(String[] args) {
        DoiTuongCha cha = new DoiTuongCha();
        cha.truyCapNoiBo();
        // Dòng dưới đây sẽ gây lỗi biên dịch nếu được bỏ comment
        // DoiTuongCha.DoiTuongConRiengTu con = new DoiTuongCha().new DoiTuongConRiengTu();
    }
}

Nếu bạn bỏ comment dòng khởi tạo DoiTuongConRiengTu trong main, trình biên dịch sẽ báo lỗi vì DoiTuongConRiengTu là private.

Lớp nội trong Lớp trừu tượng và Giao diện

Lớp nội không chỉ có thể là các lớp cụ thể mà còn có thể là lớp trừu tượng hoặc thậm chí là giao diện. Cấu trúc này mở rộng khả năng tổ chức mã logic.

Ví dụ về Lớp nội trừu tượng:

abstract class QuyTrinhCha { // Lớp trừu tượng bên ngoài
    public abstract void batDauQuyTrinh();

    abstract class BuocTruuTuong { // Lớp nội trừu tượng
        public abstract void thucHienBuoc();
    }

    class BuocCuThe { // Lớp nội cụ thể
        public void inThongBao() {
            System.out.println("Đang thực hiện một bước cụ thể.");
        }
    }
}

class TrienKhaiQuyTrinh extends QuyTrinhCha { // Triển khai lớp cha trừu tượng
    @Override
    public void batDauQuyTrinh() {
        System.out.println("Bắt đầu quy trình chính...");
        new BuocCuThe().inThongBao(); // Sử dụng lớp nội cụ thể
    }

    class TrienKhaiBuoc extends BuocTruuTuong { // Triển khai lớp nội trừu tượng
        @Override
        public void thucHienBuoc() {
            System.out.println("Thực hiện một bước trong quy trình.");
        }
    }
}

public class ChuongTrinhLopNoiTruuTuong {
    public static void main(String[] args) {
        TrienKhaiQuyTrinh quyTrinh = new TrienKhaiQuyTrinh();
        quyTrinh.batDauQuyTrinh();

        // Khởi tạo và sử dụng lớp nội trừu tượng đã được triển khai
        QuyTrinhCha.BuocTruuTuong buoc = quyTrinh.new TrienKhaiBuoc();
        buoc.thucHienBuoc();
    }
}

Kết quả:

Bắt đầu quy trình chính...
Đang thực hiện một bước cụ thể.
Thực hiện một bước trong quy trình.

Ví dụ về Giao diện nội:

interface GiaoDienCapTren {
    void batDauHoatDong();

    interface GiaoDienCapDuoi { // Giao diện nội
        void thucHienChiTiet();
    }

    class BoTro { // Lớp nội cụ thể trong giao diện
        public void inChuoi() {
            System.out.println("--- Đang hỗ trợ ---");
        }
    }
}

class TrienKhaiCapTren implements GiaoDienCapTren {
    @Override
    public void batDauHoatDong() {
        System.out.println("Hoạt động cấp trên được kích hoạt.");
    }

    class TrienKhaiCapDuoi implements GiaoDienCapDuoi { // Lớp triển khai giao diện nội
        @Override
        public void thucHienChiTiet() {
            new BoTro().inChuoi(); // Sử dụng lớp BoTro
            System.out.println("Hoạt động chi tiết đã hoàn thành.");
        }
    }
}

public class ChuongTrinhGiaoDienNoi {
    public static void main(String[] args) {
        TrienKhaiCapTren capTren = new TrienKhaiCapTren();
        capTren.batDauHoatDong();

        GiaoDienCapTren.GiaoDienCapDuoi capDuoi = capTren.new TrienKhaiCapDuoi();
        capDuoi.thucHienChiTiet();
    }
}

Kết quả:

Hoạt động cấp trên được kích hoạt.
--- Đang hỗ trợ ---
Hoạt động chi tiết đã hoàn thành.

Lớp nội tĩnh (Static Inner Class)

Khi một lớp nội được khai báo với từ khóa static, nó được gọi là lớp nội tĩnh (hoặc lớp lồng nhau tĩnh). Lớp nội tĩnh không liên kết với một thể hiện cụ thể của lớp bên ngoài mà liên kết trực tiếp với chính lớp bên ngoài. Do đó:

  • Nó không thể truy cập trực tiếp các thành viên không tĩnh của lớp bên ngoài.
  • Nó có thể truy cập các thành viên tĩnh của lớp bên ngoài.
  • Việc khởi tạo nó không cần một đối tượng của lớp bên ngoài.
class DoiTuongMe { // Lớp bên ngoài
    private static String tenGiaDinh = "Gia đình Java"; // Thành viên tĩnh
    private String tenThanhVien = "Thành viên"; // Thành viên không tĩnh

    static class DoiTuongConTinh { // Lớp nội tĩnh
        public void hienThiTenGiaDinh() {
            System.out.println("Tên gia đình: " + tenGiaDinh); // Truy cập thành viên tĩnh
            // Dòng dưới đây sẽ gây lỗi biên dịch: Không thể truy cập thành viên không tĩnh từ lớp tĩnh
            // System.out.println("Tên thành viên: " + tenThanhVien);
        }
    }
}

public class LopNoiTinh {
    public static void main(String[] args) {
        // Khởi tạo lớp nội tĩnh không cần đối tượng của lớp bên ngoài
        DoiTuongMe.DoiTuongConTinh con = new DoiTuongMe.DoiTuongConTinh();
        con.hienThiTenGiaDinh();
    }
}

Kết quả:

Tên gia đình: Gia đình Java

Lớp nội tĩnh thường được sử dụng khi bạn muốn một lớp lồng nhau có liên quan logic đến lớp bên ngoài nhưng không cần một thể hiện của lớp bên ngoài.

Bạn cũng có thể định nghĩa một giao diện nội tĩnh bên trong một giao diện khác:

interface MenuChinh {
    static interface LuaChonPhu { // Giao diện nội tĩnh
        void thucHienLuaChon();
    }
}

class TrienKhaiLuaChon implements MenuChinh.LuaChonPhu {
    @Override
    public void thucHienLuaChon() {
        System.out.println("--- Lựa chọn phụ đã được thực hiện ---");
    }
}

public class GiaoDienNoiTinh {
    public static void main(String[] args) {
        // Khởi tạo và sử dụng giao diện nội tĩnh
        MenuChinh.LuaChonPhu luaChon = new TrienKhaiLuaChon();
        luaChon.thucHienLuaChon();
    }
}

Kết quả:

--- Lựa chọn phụ đã được thực hiện ---

Lớp nội cục bộ (Method-Local Inner Class)

Lớp nội cục bộ là các lớp được định nghĩa bên trong một phương thức, một khối lệnh hoặc một hàm tạo. Phạm vi của chúng bị giới hạn trong khối mà chúng được định nghĩa. Chúng có thể truy cập các thành viên của lớp bên ngoài và các biến cục bộ (tham số phương thức hoặc biến cục bộ) của khối chứa chúng. Từ JDK 1.8 trở đi, các biến cục bộ này không cần phải được khai báo final rõ ràng, nhưng chúng vẫn phải là effectively final (nghĩa là giá trị của chúng không thay đổi sau khi được gán lần đầu).

class QuanLyDichVu { // Lớp bên ngoài
    private String tenDichVu = "Dịch vụ khách hàng cao cấp";

    public void cungCapDichVu(int idKhachHang) { // Phương thức chứa lớp nội cục bộ
        double giaTriHopDong = 15000.0; // Biến cục bộ (effectively final)

        class XuLyNoiBo { // Lớp nội cục bộ
            public void inThongTinDichVu() {
                System.out.println("Tên dịch vụ: " + QuanLyDichVu.this.tenDichVu); // Truy cập thành viên lớp ngoài
                System.out.println("ID Khách hàng: " + idKhachHang); // Truy cập tham số phương thức
                System.out.println("Giá trị hợp đồng: " + giaTriHopDong); // Truy cập biến cục bộ
            }
        }

        XuLyNoiBo xuLy = new XuLyNoiBo(); // Khởi tạo lớp nội cục bộ
        xuLy.inThongTinDichVu();
    }
}

public class LopNoiCucBo {
    public static void main(String[] args) {
        QuanLyDichVu dichVu = new QuanLyDichVu();
        dichVu.cungCapDichVu(101);
    }
}

Kết quả:

Tên dịch vụ: Dịch vụ khách hàng cao cấp
ID Khách hàng: 101
Giá trị hợp đồng: 15000.0

Lớp nội ẩn danh (Anonymous Inner Class)

Lớp nội ẩn danh là một lớp nội không có tên. Chúng được định nghĩa và khởi tạo cùng một lúc. Lớp nội ẩn danh thường được sử dụng khi bạn cần triển khai một giao diện hoặc một lớp trừu tượng chỉ một lần, tại một vị trí cụ thể trong mã.

Hãy xem xét một ví dụ mà không sử dụng lớp nội ẩn danh:

interface IThucHien { // Định nghĩa một giao diện
    void hanhDong();
}

class TrienKhaiMotLan implements IThucHien { // Triển khai giao diện
    @Override
    public void hanhDong() {
        System.out.println("--- Thực hiện hành động theo cách truyền thống ---");
    }
}

public class ChuongTrinhKhongAnDanh {
    public static void main(String[] args) {
        IThucHien doiTuong = new TrienKhaiMotLan();
        doiTuong.hanhDong();
    }
}

Nếu lớp TrienKhaiMotLan chỉ được sử dụng một lần, việc định nghĩa nó thành một lớp riêng biệt có thể gây dư thừa. Đây là lúc lớp nội ẩn danh trở nên hữu ích.

interface IThucHien {
    void hanhDong();
}

public class ChuongTrinhAnDanh {
    public static void main(String[] args) {
        // Sử dụng lớp nội ẩn danh để triển khai giao diện IThucHien
        IThucHien doiTuongAnDanh = new IThucHien() { // Định nghĩa và khởi tạo cùng lúc
            @Override
            public void hanhDong() {
                System.out.println("--- Thực hiện hành động thông qua lớp nội ẩn danh ---");
            }
        };
        doiTuongAnDanh.hanhDong();
    }
}

Kết quả:

--- Thực hiện hành động thông qua lớp nội ẩn danh ---

Lớp nội ẩn danh cũng có thể được sử dụng để mở rộng một lớp trừu tượng:

abstract class LopTruuTuongCoBan {
    private String thongDiepNoiBo = "Dữ liệu từ lớp trừu tượng.";

    public String layThongDiep() {
        return this.thongDiepNoiBo;
    }
    public abstract void inThongDiep();
}

public class ChuongTrinhAnDanhTruuTuong {
    public static void main(String[] args) {
        // Sử dụng lớp nội ẩn danh để triển khai lớp trừu tượng LopTruuTuongCoBan
        LopTruuTuongCoBan lopAnDanh = new LopTruuTuongCoBan() {
            @Override
            public void inThongDiep() {
                System.out.println(this.layThongDiep()); // Truy cập phương thức của lớp trừu tượng
            }
        };
        lopAnDanh.inThongDiep();
    }
}

Lớp nội ẩn danh rất phổ biến trong lập trình đa luồng (ví dụ: khi triển khai giao diện Runnable) và là tiền thân của các biểu thức Lambda trong Java 8, giúp mã nguồn trở nên gọn gàng và dễ đọc hơn khi xử lý các trường hợp sử dụng một lần.

Thẻ: Java Inner Classes Nested Classes Anonymous Classes Static Inner Classes

Đăng vào ngày 19 tháng 10 lúc 03:19