Статьи

Множественное наследование в Java и композиция против наследования

Некоторое время назад я написал несколько постов о наследовании , интерфейсе и композиции в Java. В этой статье мы рассмотрим множественное наследование, а затем узнаем о преимуществах композиции по сравнению с наследованием.

Множественное наследование в Java

Множественное наследование – это возможность создания одного класса с несколькими суперклассами. В отличие от некоторых других популярных объектно-ориентированных языков программирования, таких как C ++, Java не обеспечивает поддержку множественного наследования в классах . Java не поддерживает множественное наследование в классах, потому что это может привести к проблеме с бриллиантами, и вместо того, чтобы предоставлять какой-то сложный способ ее решения, существуют более эффективные способы, с помощью которых мы можем достичь того же результата, что и множественное наследование.

Алмазная проблема

Чтобы легко понять проблему алмазов, давайте предположим, что множественное наследование было поддержано в Java. В этом случае у нас может быть иерархия классов, как показано ниже.

алмазным проблем множественного наследования

Допустим, SuperClass – это абстрактный класс, объявляющий некоторый метод, а ClassA, ClassB – это конкретные классы.

SuperClass.java

1
2
3
4
5
6
package com.journaldev.inheritance;
 
public abstract class SuperClass {
 
    public abstract void doSomething();
}

ClassA.java

01
02
03
04
05
06
07
08
09
10
11
12
13
14
package com.journaldev.inheritance;
 
public class ClassA extends SuperClass{
 
    @Override
    public void doSomething(){
        System.out.println("doSomething implementation of A");
    }
 
    //ClassA own method
    public void methodA(){
 
    }
}

ClassB.java

01
02
03
04
05
06
07
08
09
10
11
12
13
14
package com.journaldev.inheritance;
 
public class ClassB extends SuperClass{
 
    @Override
    public void doSomething(){
        System.out.println("doSomething implementation of B");
    }
 
    //ClassB specific method
    public void methodB(){
 
    }
}

Теперь предположим, что реализация ClassC выглядит примерно так, как показано ниже, и она расширяет как ClassA, так и ClassB.

ClassC.java

01
02
03
04
05
06
07
08
09
10
package com.journaldev.inheritance;
 
public class ClassC extends ClassA, ClassB{
 
    public void test(){
        //calling super class method
        doSomething();
    }
 
}

Обратите внимание, что метод test() вызывает метод суперкласса doSomething() , что приводит к неоднозначности, поскольку компилятор не знает, какой метод суперкласса следует выполнить, и из-за диаграммы классов в форме ромба он называется Diamond Problem, и это Основная причина, по которой Java не поддерживает множественное наследование в классах.

Обратите внимание, что описанная выше проблема с наследованием нескольких классов также может идти только с тремя классами, у всех из которых есть хотя бы один общий метод.

Множественное наследование в интерфейсах

Вы могли заметить, что я всегда говорю, что множественное наследование не поддерживается в классах, но оно поддерживается в интерфейсах, и один интерфейс может расширять несколько интерфейсов, ниже приведен простой пример.

InterfaceA.java

1
2
3
4
5
6
package com.journaldev.inheritance;
 
public interface InterfaceA {
 
    public void doSomething();
}

InterfaceB.java

1
2
3
4
5
6
package com.journaldev.inheritance;
 
public interface InterfaceB {
 
    public void doSomething();
}

Обратите внимание, что оба интерфейса объявляют один и тот же метод, теперь у нас может быть интерфейс, расширяющий оба этих интерфейса, как показано ниже.

InterfaceC.java

1
2
3
4
5
6
7
8
package com.journaldev.inheritance;
 
public interface InterfaceC extends InterfaceA, InterfaceB {
 
    //same method is declared in InterfaceA and InterfaceB both
    public void doSomething();
 
}

Это прекрасно, потому что интерфейсы только объявляют методы, и фактическая реализация будет выполняться конкретными классами, реализующими интерфейсы, поэтому нет никакой возможности двусмысленности в множественном наследовании в интерфейсе.

Вот почему класс Java может реализовать множественное наследование, что-то вроде приведенного ниже примера.

InterfacesImpl.java

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
package com.journaldev.inheritance;
 
public class InterfacesImpl implements InterfaceA, InterfaceB, InterfaceC {
 
    @Override
    public void doSomething() {
        System.out.println("doSomething implementation of concrete class");
    }
 
    public static void main(String[] args) {
        InterfaceA objA = new InterfacesImpl();
        InterfaceB objB = new InterfacesImpl();
        InterfaceC objC = new InterfacesImpl();
 
        //all the method calls below are going to same concrete implementation
        objA.doSomething();
        objB.doSomething();
        objC.doSomething();
    }
 
}

Вы заметили, что каждый раз, когда я переопределяю любой метод суперкласса или реализую какой-либо интерфейсный метод, я использую аннотацию @Override, это одна из трех встроенных аннотаций Java, и мы всегда должны использовать аннотацию переопределения при переопределении любого метода .

Композиция для спасения

Итак, что делать, если мы хотим использовать ClassA methodA () и функцию ClassB functionB () в ClassC , решение заключается в использовании композиции , здесь представлена ​​переработанная версия ClassC, которая использует композицию для использования обоих методов классов, а также с помощью doSomething () метод из одного из объектов.

ClassC.java

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
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();
    }
}

Композиция против наследования

Одна из лучших практик Java-программирования – «отдавать предпочтение композиции поверх интерфейсов», мы рассмотрим некоторые аспекты, поддерживающие этот подход.

  1. Предположим, у нас есть суперкласс и подкласс следующим образом:

    ClassC.java

    1
    2
    3
    4
    5
    6
    7
    package com.journaldev.inheritance;
     
    public class ClassC{
     
        public void methodC(){
        }
    }

    ClassD.java

    1
    2
    3
    4
    5
    6
    7
    8
    package com.journaldev.inheritance;
     
    public class ClassD extends ClassC{
     
        public int test(){
            return 0;
        }
    }

    Приведенный выше код компилируется и работает нормально, но что, если реализация ClassC будет изменена, как показано ниже:

    ClassC.java

    01
    02
    03
    04
    05
    06
    07
    08
    09
    10
    package com.journaldev.inheritance;
     
    public class ClassC{
     
        public void methodC(){
        }
     
        public void test(){
        }
    }

    Обратите внимание, что метод test () уже существует в подклассе, но тип возвращаемого значения отличается, теперь ClassD не будет компилироваться, и если вы используете какую-либо IDE, он предложит вам изменить тип возвращаемого значения либо в суперклассе, либо в подклассе.

    Теперь представьте ситуацию, когда у нас есть многоуровневое наследование классов, и суперкласс не контролируется нами, у нас не будет другого выбора, кроме как изменить сигнатуру метода подкласса или его имя, чтобы удалить ошибку компиляции, также нам придется вносить изменения во все места, где вызывался наш метод подкласса, поэтому наследование делает наш код хрупким.

    Вышеупомянутая проблема никогда не возникнет с композицией, и это делает ее более благоприятной по сравнению с наследованием.

  2. Другая проблема с наследованием состоит в том, что мы открываем все методы суперкласса клиенту, и если наш суперкласс не спроектирован должным образом и есть дыры в безопасности, то даже несмотря на то, что мы полностью заботимся о реализации нашего класса, мы страдаем от плохой реализации суперкласс.
    Композиция помогает нам в обеспечении контролируемого доступа к методам суперкласса, тогда как наследование не обеспечивает какого-либо контроля над методами суперкласса, это также является одним из основных преимуществ композиции по сравнению с наследованием.
  3. Другое преимущество композиции заключается в том, что она обеспечивает гибкость при вызове методов. Наша реализация ClassC выше не является оптимальной и обеспечивает привязку времени компиляции с методом, который будет вызван, с минимальными изменениями мы можем сделать вызов метода гибким и сделать его динамичным.

    ClassC.java

    01
    02
    03
    04
    05
    06
    07
    08
    09
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    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();
        }
    }

    Вывод вышеуказанной программы:

    1
    2
    doSomething implementation of A
    doSomething implementation of B

    Такая гибкость при вызове метода недоступна при наследовании и повышает эффективность, чтобы отдавать предпочтение композиции по сравнению с наследованием.

  4. Модульное тестирование легко по составу, потому что мы знаем, какие все методы мы используем из суперкласса, и мы можем макетировать его для тестирования, тогда как в наследовании мы сильно зависим от суперкласса и не знаем, какие будут использоваться все методы суперкласса, поэтому нам нужно протестировать все методы суперкласса, это дополнительная работа, и мы должны делать это без необходимости из-за наследования.

В идеале мы должны использовать наследование только тогда, когда отношение « is-a » сохраняется для суперкласса и подкласса во всех случаях, иначе мы должны продолжить композицию.