Lớp ẩn danh

JAVA 25 SELF
Mức độ , Bài học
Có sẵn

1. Làm quen với lớp ẩn danh

Hãy tưởng tượng bạn có lớp Animal, mô tả cách một con vật cư xử. Lớp này có phương thức say(), phương thức này in ra "Con vật phát ra âm thanh". Bạn cần tạo một đối tượng cư xử như chó và in ra "Gâu!", nhưng bạn không muốn tạo riêng một tệp Dog.java cho việc đó.

Đây là lúc lớp ẩn danh phát huy tác dụng. Đó là các lớp không có tên, được khai báo và tạo ngay tại nơi chúng được sử dụng. 🚀 Chúng cho phép bạn “kế thừa tại chỗ” mà không cần tạo tệp riêng. Thay vì sinh ra nhiều tệp nhỏ chỉ dùng ở một chỗ, bạn có thể viết toàn bộ logic ngay tại nơi cần dùng.

Cú pháp trông như thế nào?

Cú pháp của lớp ẩn danh có thể trông hơi lạ, nhưng thực ra khá đơn giản:

TipPeremennoi imya = new TipDlyaNasledovaniya() {
    // Ở đây chúng ta viết thân của lớp ẩn danh
    // Ghi đè các phương thức của lớp cha
};

Hãy phân tích cú pháp này:

  • TipPeremennoi imya: Đây là khai báo biến thông thường, nơi chúng ta sẽ lưu đối tượng mới.
  • new TipDlyaNasledovaniya(): Ở đây trông như thể chúng ta đang tạo một đối tượng mới. Nhưng thay vì tên lớp mới, ta dùng tên lớp muốn kế thừa. Lưu ý, sau dấu ngoặc () không có dấu chấm phẩy !
  • { ... }: Tại đây ta mở ngoặc nhọn và viết toàn bộ logic của lớp ẩn danh. Có thể ghi đè các phương thức của lớp cha hoặc bổ sung logic riêng.

Ví dụ kế thừa từ một lớp thông thường:

Quay lại ví dụ về động vật.

class Animal {
    void say() {
        System.out.println("Con vật phát ra âm thanh");
    }
}

// Tạo lớp ẩn danh, kế thừa từ Animal
Animal dog = new Animal() {
    // Ghi đè phương thức say()
    @Override
    void say() {
        System.out.println("Gâu gâu! 🐶");
    }
};

Animal cat = new Animal() {
    @Override
    void say() {
        System.out.println("Meo meo! 🐱");
    }
};

dog.say(); // Sẽ in: Gâu gâu! 🐶
cat.say(); // Sẽ in: Meo meo! 🐱

Trong ví dụ này, chúng ta đã tạo hai đối tượng, dogcat, về bản chất là các lớp ẩn danh kế thừa từ Animal. Đồng thời, chúng ta không tạo bất kỳ tệp riêng nào.

So sánh các cách tiếp cận

// Cách thông thường (tạo một lớp riêng)
class Dog extends Animal {
@Override
void say() { System.out.println("Gâu!"); }
}
Animal dog = new Dog();

// VS

// Lớp ẩn danh (tất cả tại một chỗ)
Animal dog = new Animal() {
@Override
void say() { System.out.println("Gâu!"); }
};

Kết quả giống nhau, nhưng lớp ẩn danh ngắn gọn hơn và không làm rác dự án!

2. Tên của lớp ẩn danh sau khi biên dịch

Lớp ẩn danh không có tên trong mã nguồn, nhưng trình biên dịch Java dĩ nhiên vẫn phải đặt tên cho chúng để tạo tệp .class. Nó làm điều này theo một quy tắc xác định rõ:

  • Tên tệp lớp ẩn danh bao gồm tên của lớp bao ngoài nơi nó được khai báo.
  • Sau tên lớp bao ngoài là ký hiệu đô la $.
  • Tiếp theo là số thứ tự của lớp ẩn danh trong tệp đó, bắt đầu từ 1.

Vì vậy, nếu ví dụ về động vật của chúng ta nằm trong tệp Main.java, thì sau khi biên dịch sẽ có ba tệp được tạo:

  • Main.class
  • Main$1.class (lớp ẩn danh cho chó)
  • Main$2.class (lớp ẩn danh cho mèo)

Nếu lớp ẩn danh được khai báo bên trong một phương thức, mà phương thức này lại nằm trong một lớp lồng bên trong, tên sẽ trông phức tạp hơn, ví dụ OuterClass$InnerClass$1.class.

Đây là quy ước nội bộ của trình biên dịch — hữu ích để biết, nhưng hiếm khi quan trọng trong công việc hằng ngày. Điều chính cần nhớ là lớp ẩn danh vẫn là một lớp “đúng nghĩa”, chỉ là không có tên trong mã nguồn.

3. Đặc điểm quan trọng và hạn chế

Lớp ẩn danh là công cụ mạnh mẽ, nhưng cũng có các quy tắc riêng.

Truy cập biến. Lớp ẩn danh có thể sử dụng các biến từ phương thức bao quanh nó. Tuy nhiên, các biến này phải là final hoặc effectively final (tức là giá trị của chúng không thay đổi sau khi khởi tạo).

public void doSomething() {
    String greeting = "Xin chào!"; // Biến này effectively final

    class OuterClass {
        void greet() {
            // Tạo lớp ẩn danh bên trong phương thức
            new Object() {
                void sayHello() {
                    System.out.println(greeting); // Hợp lệ
                    // greeting = "Tạm biệt!"; // Điều này sẽ gây lỗi!
                }
            }.sayHello();
        }
    }

    new OuterClass().greet();
}

Tại sao lại như vậy? Bởi vì lớp ẩn danh có thể “sống” lâu hơn chính phương thức, và nếu nó có thể thay đổi biến đó, điều này sẽ dẫn đến vấn đề.

Không có constructor. Vì lớp ẩn danh không có tên nên cũng không thể có constructor. Tuy nhiên, bạn có thể dùng khối khởi tạo để thực thi mã khi tạo đối tượng:

Animal dog = new Animal() {
    // Khối khởi tạo
    {
        System.out.println("Khởi tạo lớp ẩn danh 🐶");
    }
    @Override
    void say() {
        System.out.println("Gâu gâu!");
    }
};

Hạn chế. Lớp ẩn danh không thể khai báo trường tĩnh (ngoại trừ hằng số) hoặc phương thức tĩnh. Chúng luôn được tạo như một phần của đối tượng khác, vì thế không thể có các sửa đổi static, public, protected hoặc private.

4. Những điểm hữu ích

Khi nào nên dùng lớp ẩn danh

  • Bạn cần kế thừa một lớp (hoặc triển khai interface) chỉ một lần.
  • Phần triển khai nhỏ — 1–2 phương thức, không quá vài chục dòng.
  • Lớp chỉ cần ở một nơi và không đáng để đặt tên riêng.
  • Bạn muốn tránh làm bừa bộn package với vô số lớp nhỏ dùng một lần.

Kịch bản điển hình:

  • Trình xử lý sự kiện (GUI, Swing, Android, v.v.).
  • Truyền callback vào các phương thức.
  • Cài đặt Comparator nhanh để sắp xếp các collection.
  • Đối tượng tạm thời đã chỉnh sửa (ví dụ cho test).

5. Tương tác với lớp bao ngoài

Nếu lớp ẩn danh được khai báo bên trong một phương thức hoặc khối không tĩnh của lớp bao ngoài, nó có thể truy cập các trường và phương thức của lớp bao ngoài (kể cả private!).

public class Outer {
    private String secret = "Văn bản bí mật";

    // Lớp cơ sở
    class Printer {
        public void print() {
            System.out.println("In thông thường");
        }
    }

    public void revealSecret() {
        Printer p = new Printer() {
            @Override
            public void print() {
                System.out.println("Truy cập private: " + secret);
            }
        };
        p.print();
    }

    public static void main(String[] args) {
        new Outer().revealSecret();
    }
}

6. Lỗi thường gặp khi làm việc với lớp ẩn danh

Lỗi số 1: cố gắng thay đổi biến từ phương thức bao quanh.
Nếu bạn khai báo một biến bên ngoài lớp ẩn danh và cố gắng thay đổi nó sau khi đã dùng bên trong lớp ẩn danh — trình biên dịch sẽ báo lỗi. Biến phải là final hoặc effectively final (không thay đổi sau khi khởi tạo).

Lỗi số 2: lớp ẩn danh quá lớn.
Nếu lớp ẩn danh phình ra đến hàng chục dòng và chứa nhiều phương thức — đó là tín hiệu bạn nên tách nó thành một lớp có tên riêng. Nếu không, mã sẽ trở nên khó đọc.

Lỗi số 3: cố gắng sử dụng phương thức hoặc trường tĩnh.
Trong lớp ẩn danh không thể khai báo phương thức hoặc trường tĩnh (trừ hằng số). Nếu thật sự cần — hãy tạo lớp lồng thông thường.

Lỗi số 4: quên phạm vi khả kiến.
Lớp ẩn danh chỉ nhìn thấy tại nơi nó được khai báo và không có tên. Nếu cần sử dụng nhiều lần — hãy khai báo một lớp thông thường.

1
Nhiệm vụ
JAVA 25 SELF, mức độ, bài học
Đã khóa
Nông trại ma thuật: âm thanh độc đáo của sinh vật 🪄
Nông trại ma thuật: âm thanh độc đáo của sinh vật 🪄
1
Nhiệm vụ
JAVA 25 SELF, mức độ, bài học
Đã khóa
Trình dịch liên thiên hà: lời chào tức thì 🚀
Trình dịch liên thiên hà: lời chào tức thì 🚀
1
Nhiệm vụ
JAVA 25 SELF, mức độ, bài học
Đã khóa
Dây chuyền sản xuất: thông báo bắt đầu hoạt động 🏭
Dây chuyền sản xuất: thông báo bắt đầu hoạt động 🏭
1
Nhiệm vụ
JAVA 25 SELF, mức độ, bài học
Đã khóa
Phòng thí nghiệm bí mật: tiết lộ thông điệp được mã hóa 🕵️
Phòng thí nghiệm bí mật: tiết lộ thông điệp được mã hóa 🕵️
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION