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.