1. Lớp bên trong (inner class) là gì?
Trong Java, một lớp có thể được khai báo không chỉ ở cấp cao nhất (trong file) mà còn bên trong một lớp khác. Lớp như vậy được gọi là lớp lồng nhau (nested class). Nếu lớp đó được khai báo không có modifier static, thì nó được gọi là lớp bên trong (non-static inner class, hoặc đơn giản là inner class).
Lớp bên trong là lớp được khai báo bên trong một lớp khác và gắn với một instance của lớp bên ngoài đó. Nó có thể truy cập mọi field và method của lớp bên ngoài, ngay cả khi chúng là private. Hãy hình dung đối tượng có một “trợ lý bí mật” của riêng mình, được phép làm mọi thứ!
Tương tự như một ngôi nhà và các căn phòng. Ngôi nhà là lớp bên ngoài, còn căn phòng là lớp bên trong. Phòng không thể tồn tại nếu không có nhà, nhưng vẫn có quyền truy cập tài nguyên của nhà: điện, sưởi, đồ nội thất. Nếu ngôi nhà biến mất, căn phòng cũng biến mất. Lớp bên trong trong Java hoạt động chính xác như vậy!
Tại sao cần lớp bên trong?
- Liên kết logic: Khi một lớp chỉ cần thiết để làm việc cùng một lớp khác và không có ý nghĩa bên ngoài lớp đó.
- Đóng gói: Cho phép ẩn chi tiết triển khai, không làm “bẩn” không gian tên của package.
- Truy cập thành viên private: Lớp bên trong có thể truy cập các field và method private của lớp bên ngoài.
- Gọn gàng: Giảm số lượng lớp “rác” ở cấp package.
Cú pháp khai báo lớp bên trong
Khai báo lớp bên trong rất đơn giản: nó được khai báo bên trong thân của một lớp khác, không có modifier static.
class Outer {
// các trường và phương thức của lớp bên ngoài
class Inner {
// các trường và phương thức của lớp bên trong
void printHello() {
System.out.println("Hello from Inner!");
}
}
}
Ở đây Inner là lớp bên trong so với Outer.
Hình minh họa: trực quan
Outer
│
├─ trường/phương thức
│
└─ Inner (lớp bên trong)
└─ các trường/phương thức riêng
2. Cách tạo đối tượng (instance) của lớp bên trong
Đây là một trong những chiếc bẫy phổ biến nhất cho người mới! Instance của lớp bên trong luôn gắn với một đối tượng cụ thể của lớp bên ngoài.
Ví dụ:
Outer outer = new Outer(); // tạo đối tượng của lớp bên ngoài
Outer.Inner inner = outer.new Inner(); // tạo lớp bên trong thông qua đối tượng bên ngoài!
inner.printHello(); // Hello from Inner!
Cố gắng gọi đơn thuần new Inner() sẽ dẫn tới lỗi biên dịch, vì Java không biết phải gắn lớp bên trong này với đối tượng lớp bên ngoài nào.
Tại sao lại như vậy?
Lớp bên trong có thể truy cập các field và method của lớp bên ngoài. Vì vậy, nó cần “biết” đang làm việc với đối tượng lớp bên ngoài nào.
3. Ví dụ sử dụng lớp bên trong
Hãy xem một ví dụ nơi lớp bên trong thực sự hữu ích.
Ví dụ 1: Mô hình “Ba lô và vật phẩm”
Giả sử chúng ta có lớp Backpack có thể chứa các vật phẩm (Item). Nhưng ta muốn lớp Item chỉ khả dụng bên trong Backpack, vì bên ngoài ba lô thì vật phẩm không liên quan.
public class Backpack {
private String owner;
public Backpack(String owner) {
this.owner = owner;
}
// Lớp bên trong — vật phẩm chỉ có thể tồn tại trong ba lô!
class Item {
private String name;
public Item(String name) {
this.name = name;
}
public void printInfo() {
// “Phép màu”! Thấy được field private của lớp bên ngoài
System.out.println(owner + " có vật phẩm: " + name);
}
}
}
Sử dụng:
Backpack bp = new Backpack("Vasya");
Backpack.Item item = bp.new Item("Giáo trình Java");
item.printInfo(); // Vasya có vật phẩm: Giáo trình Java
Lưu ý: Item có thể truy cập field owner, ngay cả khi nó là private!
Ví dụ 2: Iterator cho collection tự viết
Trong Java, các collection thường triển khai iterator dưới dạng lớp bên trong. Hãy thử viết một collection đơn giản với lớp iterator bên trong.
public class IntList {
private int[] data = new int[10];
private int size = 0;
public void add(int value) {
data[size++] = value;
}
// Lớp bên trong — iterator!
class Iterator {
private int index = 0;
public boolean hasNext() {
return index < size;
}
public int next() {
return data[index++];
}
}
}
Sử dụng:
IntList list = new IntList();
list.add(10);
list.add(20);
IntList.Iterator it = list.new Iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
4. Đặc điểm của các lớp bên trong
Truy cập các thành viên của lớp bên ngoài
Lớp bên trong có thể truy cập bất kỳ field và method nào (kể cả private) của lớp bên ngoài. Điều này tiện lợi nhưng cần thận trọng: nếu lớp bên ngoài thay đổi, lớp bên trong có thể đột ngột “hỏng”.
class Outer {
private int secret = 42;
class Inner {
void showSecret() {
System.out.println("Bí mật: " + secret);
}
}
}
Lớp bên trong không thể chứa thành viên static
Lớp bên trong (non-static inner) không thể chứa các field hoặc method static, ngoại trừ các hằng số static final (ví dụ, public static final int MY_CONST = 123;). Lý do là lớp bên trong luôn “gắn” với một instance của lớp bên ngoài.
Nếu bạn cần khai báo lớp lồng nhau dạng static — hãy dùng modifier static (sẽ bàn trong bài kế tiếp).
Phạm vi hiển thị của lớp bên trong
Lớp bên trong có thể là private, protected, public hoặc có phạm vi mặc định ở mức package (package-private).
public class Outer {
private class Inner { /* ... */ }
}
5. Những điểm hữu ích
Khi nào nên dùng lớp bên trong
Lớp bên trong không phải để “làm đẹp” mà để tổ chức mã một cách logic. Hãy dùng khi:
- Lớp chỉ cần ở một nơi (ví dụ: iterator trợ giúp, handler, thành phần của cấu trúc phức tạp).
- Lớp gắn chặt với lớp bên ngoài và không có ý nghĩa bên ngoài nó.
- Muốn ẩn chi tiết triển khai khỏi các lớp khác trong package.
Đừng dùng lớp bên trong chỉ vì “mốt”! Đôi khi tốt hơn nên đưa lớp lên cấp cao nhất nếu nó có thể hữu ích ở nơi khác.
Lớp bên trong và this
Bên trong lớp bên trong, bạn có thể truy cập các field và method của lớp bên ngoài bằng OuterClassName.this.
class Car {
private String model = "Tesla";
class Engine {
void printModel() {
// Truy cập rõ ràng tới đối tượng bên ngoài
System.out.println("Mẫu: " + Car.this.model);
}
}
}
Cú pháp này đặc biệt hữu ích khi lớp bên trong và lớp bên ngoài có các field trùng tên.
Khi KHÔNG nên dùng lớp bên trong
Không nên dùng nếu:
- Lớp bên trong không truy cập field/method của lớp bên ngoài
- Lớp có thể hữu ích ở những nơi khác trong chương trình
- Lớp bên ngoài trở nên quá lớn và phức tạp
Trong những trường hợp đó, tốt hơn nên:
- Biến lớp thành lớp lồng nhau static (static class)
- Đưa lớp ra file riêng
- Dùng các method thông thường thay vì cả một lớp
6. Các lỗi thường gặp khi làm việc với lớp bên trong
Lỗi số 1: Thử tạo lớp bên trong mà không có đối tượng của lớp bên ngoài.
Nếu viết new Inner(), trình biên dịch sẽ báo lỗi: "No enclosing instance of type Outer is accessible". Luôn tạo lớp bên trong thông qua đối tượng của lớp bên ngoài: outer.new Inner().
Lỗi số 2: Cố gắng khai báo các field hoặc method static trong lớp bên trong.
Lớp bên trong không thể chứa các thành viên static (trừ hằng số). Nếu cần — hãy dùng lớp lồng nhau static, tức là khai báo lớp với modifier static.
Lỗi số 3: Lạm dụng lớp bên trong.
Nếu lớp bên trong không dùng field/method của lớp bên ngoài, rất có thể nó nên là static hoặc nên được đưa ra ngoài. Đừng tạo lớp bên trong “cho có”.
Lỗi số 4: Nhầm lẫn khi truy cập field của lớp bên ngoài.
Nếu lớp bên trong và lớp bên ngoài có các field trùng tên, hãy dùng OuterClassName.this.field để truy cập rõ ràng field của lớp bên ngoài.
GO TO FULL VERSION