CodeGym /Các khóa học /JAVA 25 SELF /Triển khai nhiều interface

Triển khai nhiều interface

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

1. Giới thiệu

Trong Java, bạn không thể kế thừa nhiều lớp cùng lúc. Điều này được thiết kế có chủ ý để tránh “diamond of death” — tình huống hai lớp cha định nghĩa cùng một phương thức và không rõ nên lấy triển khai nào. Nhưng với interface thì bạn có thể triển khai bao nhiêu cũng được! Tại sao? Bởi vì interface là một hợp đồng; nó không chứa phần triển khai (trước Java 8 — không có triển khai; từ Java 8 — có thể có các phương thức defaultstatic). Vì vậy, sẽ không có sự rối rắm nào từ việc kế thừa mã.

Điều này giống như trong đời sống: bạn có thể đồng thời là “tài xế”, “người dùng máy tính” và “vận động viên bơi lội”. Mỗi “interface” này mô tả những kỹ năng nhất định nhưng không buộc bạn phải là bản sao của người khác.

Cú pháp triển khai nhiều interface

Trong Java, một lớp có thể triển khai nhiều interface bằng cách liệt kê chúng, phân tách bằng dấu phẩy, sau từ khóa implements. Ví dụ cơ bản:

public interface Movable {
    void move(int x, int y);
}

public interface Chargeable {
    void charge();
}

public class Robot implements Movable, Chargeable {
    @Override
    public void move(int x, int y) {
        System.out.println("Robot di chuyển tới tọa độ (" + x + ", " + y + ")");
    }

    @Override
    public void charge() {
      System.out.println("Robot đang sạc.");
    }
}

Trong ví dụ này, Robot rất đa năng: nó vừa di chuyển vừa sạc. Giống như ngoài đời: bạn càng biết nhiều — bạn càng thường được mời phỏng vấn!

2. Tại sao cần? Ví dụ thực tế

Ví dụ 1. Các “vai trò” khác nhau của một đối tượng

Hãy tưởng tượng bạn đang thiết kế một nhân vật trong game:

  • Có thể di chuyển (Movable)
  • Có thể tấn công (Attackable)
  • Có thể lưu ra tệp (Serializable — interface này có trong thư viện chuẩn của Java)
public interface Attackable {
    void attack();
}

public class Hero implements Movable, Attackable, java.io.Serializable {
    @Override
    public void move(int x, int y) {
        System.out.println("Anh hùng di chuyển đến vị trí mới.");
    }

    @Override
    public void attack() {
        System.out.println("Anh hùng tung đòn tấn công!");
    }
}

Giờ đây lớp của bạn có thể được dùng trong nhiều ngữ cảnh khác nhau: có thể truyền nó vào các phương thức yêu cầu bất kỳ interface nào trong số đó.

Ví dụ 2. Kết hợp các interface chuẩn

Trong thư viện chuẩn Java, rất thường gặp các interface Comparable (để so sánh đối tượng) và Serializable (để lưu đối tượng ra tệp hoặc truyền qua mạng). Đôi khi bạn cần đối tượng vừa là cái này, vừa là cái kia:

public class Person implements Comparable<Person>, java.io.Serializable {
    private String name;
    private int age;

    public Person(String name, int age) { this.name = name; this.age = age; }

    @Override
    public int compareTo(Person other) {
        return Integer.compare(this.age, other.age);
    }
}

Bây giờ các đối tượng Person có thể được sắp xếp (ví dụ trong danh sách) và ghi ra tệp.

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

Một triển khai cho mỗi phương thức

Nếu hai interface định nghĩa một phương thức với cùng chữ ký, bạn chỉ cần triển khai nó một lần. Ví dụ:

public interface A {
    void doSomething();
}
public interface B {
    void doSomething();
}
public class MyClass implements A, B {
    @Override
    public void doSomething() {
        System.out.println("Triển khai doSomething cho cả hai interface.");
    }
}

Java sẽ không phàn nàn — miễn là các chữ ký trùng nhau. Nếu phương thức khác nhau về chữ ký, chúng được xem là các phương thức khác nhau — bạn cần triển khai từng cái.

Không có “diamond of death”

Khác với đa kế thừa lớp, khi triển khai nhiều interface sẽ không có tình huống thừa kế hai triển khai khác nhau của cùng một phương thức. Trước Java 8, interface không hề có triển khai; còn khi xuất hiện các phương thức default — nếu phát sinh xung đột, bạn bắt buộc phải giải quyết một cách tường minh (về điều này sẽ chi tiết hơn ở bài giảng tiếp theo).

Không có trạng thái

Interface không thể chứa trường thông thường (chỉ có hằng số — public static final). Vì vậy sẽ không có sự rối rắm kiểu “hai trường của cha có cùng tên”.

4. Ví dụ: triển khai nhiều interface trong một lớp

Hãy thêm vào ứng dụng học tập của chúng ta (chẳng hạn một sở thú) các khả năng mới. Giả sử có các loài vật có thể di chuyển và phát ra âm thanh:

public interface Movable {
    void move(int x, int y);
}

public interface Soundable {
    void makeSound();
}

public class Dog implements Movable, Soundable {
    private String name;

    public Dog(String name) {
        this.name = name;
    }

    @Override
    public void move(int x, int y) {
        System.out.println(name + " chạy tới (" + x + ", " + y + ")");
    }

    @Override
    public void makeSound() {
        System.out.println(name + " nói: Gâu gâu!");
    }
}

public class Cat implements Movable, Soundable {
    private String name;

    public Cat(String name) {
        this.name = name;
    }

    @Override
    public void move(int x, int y) {
        System.out.println(name + " rón rén tới (" + x + ", " + y + ")");
    }

    @Override
    public void makeSound() {
        System.out.println(name + " nói: Meo!");
    }
}

Giờ ta có thể viết các phương thức tổng quát để làm việc với bất kỳ đối tượng “có thể di chuyển” hoặc “phát âm thanh” nào:

public static void testMovable(Movable m) {
    m.move(10, 20);
}

public static void testSoundable(Soundable s) {
    s.makeSound();
}

public static void main(String[] args) {
    Dog rex = new Dog("Rex");
    Cat murka = new Cat("Murka");

    testMovable(rex);       // Rex chạy tới (10, 20)
    testSoundable(murka);   // Murka nói: Meo!
}

Và dĩ nhiên, nếu một đối tượng triển khai cả hai interface, bạn có thể truyền nó vào cả hai nơi!

6. Những tinh tế hữu ích

Nếu các interface xung đột thì sao?

Đôi khi hai interface định nghĩa các phương thức có cùng chữ ký nhưng ý nghĩa khác nhau. Ví dụ, một interface kỳ vọng phương thức reset() sẽ đặt lại tọa độ, còn interface khác — phương thức đó sẽ tắt thiết bị. Trong trường hợp này bạn phải cẩn thận: vẫn phải triển khai phương thức ấy một lần, và nó cần “xử lý” cả hai hành vi (hoặc ít nhất chọn một cách rõ ràng). Trong thực tế những tình huống này hiếm gặp, nhưng nếu gặp — hãy cân nhắc lại tính đúng đắn của thiết kế.

Ví dụ với tập hợp các đối tượng của các interface khác nhau

Giả sử chúng ta có một danh sách đối tượng triển khai các interface khác nhau. Ta có thể duyệt qua và gọi các phương thức cần thiết:

Movable[] movables = {
    new Dog("Sharik"),
    new Cat("Barsik"),
    new Robot()
};

for (Movable m : movables) {
    m.move(0, 0);
}

Tương tự, bạn có thể làm điều này cho bất kỳ interface nào.

7. Các lỗi điển hình khi triển khai nhiều interface

Lỗi số 1: không triển khai đầy đủ tất cả các phương thức của interface.
Nếu một lớp khai báo rằng nó triển khai một interface nhưng không triển khai ít nhất một phương thức của interface đó — trình biên dịch sẽ báo lỗi ngay. Đừng quên mọi phương thức, kể cả khi chúng có vẻ “thừa”.

Lỗi số 2: các phương thức xung đột có cùng chữ ký.
Nếu hai interface định nghĩa các phương thức giống nhau, bạn chỉ cần triển khai một lần. Nhưng nếu ý nghĩa của các phương thức đó khác nhau, điều này có thể dẫn đến nhầm lẫn và bug. Trong trường hợp này, tốt hơn nên xem xét lại kiến trúc.

Lỗi số 3: cố gắng thừa kế interface bằng extends trong lớp.
Trong lớp, để triển khai interface luôn dùng implements, không phải extends. Ví dụ:

public class MyClass implements A, B { ... } // đúng
public class MyClass extends A, B { ... }    // sai!

Lỗi số 4: cố gắng tạo đối tượng của interface.
Interface là một hợp đồng; bạn không thể tạo trực tiếp nó:

Movable m = new Movable(); // lỗi biên dịch

Chỉ có thể tạo đối tượng của các lớp triển khai interface.

Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION