JavaRush /Blog Java /Random-VI /Đa kế thừa trong Java. So sánh thành phần và sự kế thừa
HonyaSaar
Mức độ
Москва

Đa kế thừa trong Java. So sánh thành phần và sự kế thừa

Xuất bản trong nhóm
Cách đây một thời gian, tôi đã viết một số bài về tính kế thừa, giao diện và thành phần trong Java. Trong bài viết này, chúng ta sẽ xem xét tính đa kế thừa và sau đó tìm hiểu về lợi ích của việc kết hợp so với kế thừa.
Đa kế thừa trong Java.  So sánh thành phần và tính kế thừa - 1

Đa kế thừa trong Java

Đa kế thừa là khả năng tạo các lớp có nhiều lớp cha. Không giống như các ngôn ngữ hướng đối tượng phổ biến khác như C++, Java không hỗ trợ kế thừa nhiều lớp. Anh ấy không ủng hộ nó vì có khả năng gặp phải “vấn đề kim cương” và thay vào đó, anh ấy muốn cung cấp một số cách tiếp cận toàn diện để giải quyết nó, sử dụng các phương án tốt nhất mà chúng tôi có thể đạt được kết quả kế thừa tương tự.

"Vấn đề kim cương"

Để hiểu vấn đề kim cương một cách đơn giản hơn, hãy giả sử rằng tính đa kế thừa được hỗ trợ trong Java. Trong trường hợp này, chúng ta có thể nhận được các lớp có hệ thống phân cấp như trong hình bên dưới. hệ thống phân cấp lớp kim cươngGiả sử đó SuperClasslà một lớp trừu tượng mô tả một phương thức nhất định và các lớp ClassAđó ClassBlà các lớp thực. SuperClass.java
package com.journaldev.inheritance;
public abstract class SuperClass {
   	public abstract void doSomething();
}
ClassA.java
package com.journaldev.inheritance;
public class ClassA extends SuperClass{
    @Override
 public void doSomething(){
        System.out.println("Какая-то реализация класса A");
    }
  //собственный метод класса  ClassA
    public void methodA(){
    }
}
Bây giờ, hãy giả sử rằng lớp ClassCkế thừa từ ClassAClassBđồng thời, đồng thời có cách triển khai sau:
package com.journaldev.inheritance;
public class ClassC extends ClassA, ClassB{
    public void test(){
        //вызов метода родительского класса
        doSomething();
    }
}
Lưu ý rằng phương thức này test()gọi một phương thức doSomething()của lớp cha, điều này sẽ dẫn đến sự mơ hồ vì trình biên dịch không biết nên gọi phương thức siêu lớp nào. Do hình dạng của sơ đồ kế thừa lớp trong tình huống này giống với đường viền của một viên kim cương được mài giác nên vấn đề này được gọi là “Vấn đề kim cương”. Đây là lý do chính tại sao Java không hỗ trợ kế thừa nhiều lớp. Lưu ý rằng vấn đề kế thừa nhiều lớp này cũng có thể xảy ra với ba lớp có ít nhất một phương thức chung.

Đa kế thừa và giao diện

Bạn có thể nhận thấy rằng tôi luôn nói "đa kế thừa không được hỗ trợ giữa các lớp", nhưng nó được hỗ trợ giữa các giao diện. Một ví dụ đơn giản được hiển thị dưới đây: InterfaceA.java
package com.journaldev.inheritance;
public interface InterfaceA {

    public void doSomething();
}
InterfaceB.java
package com.journaldev.inheritance;

public interface InterfaceB {

    public void doSomething();
}
Lưu ý rằng cả hai giao diện đều có một phương thức có cùng tên. Bây giờ giả sử chúng ta có một giao diện kế thừa từ cả hai giao diện. InterfaceC.java
package com.journaldev.inheritance;

public interface InterfaceC extends InterfaceA, InterfaceB {

    //метод, с тем же названием описан в  InterfaceA и InterfaceB
    public void doSomething();
Ở đây, mọi thứ đều lý tưởng, vì giao diện chỉ là phần đặt trước/mô tả của một phương thức và việc triển khai phương thức đó sẽ nằm trong lớp cụ thể thực hiện các giao diện này, do đó không có khả năng gặp phải sự mơ hồ với nhiều kế thừa giao diện. Đây là lý do tại sao các lớp trong Java có thể kế thừa từ nhiều giao diện. Hãy thể hiện điều đó bằng ví dụ dưới đây. InterfacesImpl.java
package com.journaldev.inheritance;

public class InterfacesImpl implements InterfaceA, InterfaceB, InterfaceC {

    @Override
    public void doSomething() {
        System.out.println("doSomething реализация реального класса ");
    }

    public static void main(String[] args) {
        InterfaceA objA = new InterfacesImpl();
        InterfaceB objB = new InterfacesImpl();
        InterfaceC objC = new InterfacesImpl();

        //все вызываемые ниже методы получат одинаковую реализацию конкретного класса

        objA.doSomething();
        objB.doSomething();
        objC.doSomething();
    }
}
Bạn có thể nhận thấy rằng mỗi lần tôi ghi đè một phương thức được mô tả trong siêu lớp hoặc trong một giao diện, tôi đều sử dụng chú thích @Override. Đây là một trong ba chú thích Java tích hợp sẵn và bạn phải luôn sử dụng nó khi ghi đè các phương thức.

Thành phần như sự cứu rỗi

Vậy nếu chúng ta muốn sử dụng một methodA()lớp ClassAvà hàm methodB()lớp ClassBtrong thì sao ClassС? Giải pháp cho vấn đề này có thể là sự kết hợp - một phiên bản viết lại ClassCthực hiện cả hai phương thức lớp ClassAClassBcũng có phần triển khai doSomething()cho một trong các đối tượng. ClassC.java
package com.journaldev.inheritance;

public class ClassC{

    ClassA objA = new ClassA();
    ClassB objB = new ClassB();

    public void test(){
        objA.doSomething();
    }

    public void methodA(){
        objA.methodA();
    }

    public void methodB(){
        objB.methodB();
    }
}

Thành phần hay thừa kế?

Thực hành lập trình Java tốt là tận dụng lợi thế của sự kết hợp thay vì kế thừa. Chúng ta sẽ xem xét một số khía cạnh có lợi cho phương pháp này.
  1. Giả sử chúng ta có sự kết hợp sau đây của các lớp cha mẹ-người thừa kế:

    ClassC.java

    package com.journaldev.inheritance;
    
    public class ClassC{
    
    public void methodC(){
      	}
    
    }

    ClassD.java

    package com.journaldev.inheritance;
    
    public class ClassD extends ClassC{
    
        public int test(){
            return 0;
        }
    }

    Đoạn mã trên biên dịch và hoạt động tốt, nhưng điều gì sẽ xảy ra nếu ClassCnó được triển khai theo cách khác:

    package com.journaldev.inheritance;
    
    public class ClassC{
    
        public void methodC(){
        }
    
        public void test(){
        }
    }

    Lưu ý rằng phương thức này test()đã tồn tại trong lớp con nhưng trả về kết quả thuộc loại khác. Bây giờ ClassD, trong trường hợp bạn đang sử dụng IDE, nó sẽ không biên dịch. Bạn sẽ được khuyên thay đổi kiểu trả về trong lớp con hoặc lớp cha.

    Bây giờ hãy tưởng tượng một tình huống trong đó có sự kế thừa đa cấp của các lớp và siêu lớp không có sẵn cho những thay đổi của chúng ta. Bây giờ, để loại bỏ lỗi biên dịch, chúng ta không có lựa chọn nào khác ngoài việc thay đổi chữ ký hoặc tên của phương thức lớp con. Chúng tôi cũng sẽ phải thực hiện thay đổi đối với tất cả những nơi phương thức này được gọi. Vì vậy, tính kế thừa làm cho mã của chúng ta dễ vỡ.

    Vấn đề được mô tả ở trên không bao giờ xảy ra trong trường hợp thành phần, và do đó làm cho thành phần sau được ưu tiên hơn là kế thừa.

  2. Vấn đề tiếp theo với tính kế thừa là chúng ta hiển thị tất cả các phương thức của cha mẹ cho máy khách. Và nếu siêu lớp không được thiết kế chính xác và chứa các lỗ hổng bảo mật. Sau đó, mặc dù chúng tôi quan tâm đầy đủ đến vấn đề bảo mật trong việc triển khai lớp con của mình, chúng tôi vẫn sẽ phụ thuộc vào việc triển khai sai sót của lớp cha.

    Thành phần giúp chúng ta cung cấp quyền truy cập có kiểm soát vào các phương thức của siêu lớp, trong khi tính kế thừa không duy trì bất kỳ quyền kiểm soát nào đối với các phương thức của nó. Đây cũng là một trong những lợi thế chính của thành phần so với kế thừa.

  3. Một lợi ích khác của việc kết hợp là nó tăng thêm tính linh hoạt khi gọi các phương thức. Việc triển khai lớp ClassCđược mô tả ở trên là không tối ưu và sử dụng liên kết sớm với phương thức được gọi. Những thay đổi tối thiểu sẽ cho phép chúng tôi thực hiện việc gọi phương thức một cách linh hoạt và cho phép liên kết muộn (liên kết khi chạy).

    ClassC.java

    package com.journaldev.inheritance;
    public class ClassC{
        SuperClass obj = null;
        public ClassC(SuperClass o){
            this.obj = o;
        }
        public void test(){
            obj.doSomething();
        }
    
        public static void main(String args[]){
            ClassC obj1 = new ClassC(new ClassA());
            ClassC obj2 = new ClassC(new ClassB());
    
            obj1.test();
            obj2.test();
        }
    }

    Chương trình trên sẽ hiển thị:

    doSomething implementation of A
    doSomething implementation of B

    Tính linh hoạt trong việc gọi phương thức này không được thấy ở tính kế thừa, điều này làm cho việc kết hợp trở thành cách tiếp cận tốt nhất.

  4. Kiểm thử đơn vị dễ dàng hơn trong trường hợp thành phần vì chúng ta biết rằng đối với tất cả các phương thức được sử dụng trong siêu lớp, chúng ta có thể bỏ qua các kiểm thử, trong khi về kế thừa, chúng ta phụ thuộc rất nhiều vào siêu lớp và không biết các phương thức của lớp cha như thế nào sẽ được sử dụng. Vì vậy, do tính kế thừa nên chúng ta sẽ phải test tất cả các phương thức của superclass, đây là công việc không cần thiết.

    Lý tưởng nhất là chỉ nên sử dụng tính kế thừa khi mối quan hệ “ is-a ” đúng với lớp cha và lớp con, nếu không thì nên ưu tiên kết hợp.

Bài báo gốc
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION