Недавно я писал несколько постов о наследовании и композиции в Java. В этой статье мы будем изучать множественное наследования, а затем узнаем о преимуществах композиции перед наследованием.
Множественное наследование в Java
Множественное наследование — возможность создания единого класса с несколькими суперклассами.
В отличие от некоторых других популярных объектно-ориентированных языках программирования, таких как C ++, Java не предоставляет поддержку множественного наследования в классах. Java не поддерживает множественное наследование классов, потому что это может привести к проблеме ромба (ромбовидное наследование) и вместо того, чтобы предоставлять сложный путь разрешения этой проблемы, придумали способ лучше.
Проблема ромба
Понять проблему с ромбами легко: давайте предположим, что множественное наследование было реализовано в Java. В этом случае, мы могли бы иметь иерархию классов, как на изображении ниже.
Давайте создадим абстрактный суперклас SuperClass с методом doSomething(), а также два класса ClassA, ClassB
SuperClass.java
1 2 3 4 5 6 |
package ua.com.prologistic.inheritance; public abstract class SuperClass { public abstract void doSomething(); } |
ClassA.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
package ua.com.prologistic.inheritance; public class ClassA extends SuperClass{ @Override public void doSomething(){ System.out.println("doSomething реализуется в классе A"); } //Собственный метод класса ClassA public void methodA(){ } } |
ClassB.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
package ua.com.prologistic.inheritance; public class ClassB extends SuperClass{ @Override public void doSomething(){ System.out.println("doSomething реализуется классом B"); } //Свой метод класса ClassB public void methodB(){ } } |
А теперь давайте создадим класс ClassC, который наследует классы ClassA и ClassB
1 2 3 4 5 6 7 8 9 10 |
package ua.com.prologistic.inheritance; public class ClassC extends ClassA, ClassB{ public void test(){ //вызываем метод суперкласса doSomething(); } } |
Обратите внимание, что метод test()
вызывает метод суперкласса doSomething()
Это приводит к неопределенности: компилятор не знает, какой метод суперкласса выполнить из-за ромбовидной формы (выше на диаграмме классов). Это называют проблемой ромба — и это основная причина почему Java не поддерживает множественное наследование классов.
Множественное наследование в интерфейсах
Вы, возможно, заметили, я всегда говорю, что множественное наследование не поддерживается в классах, но оно поддерживается в интерфейсах и единый интерфейс может наследовать несколько интерфейсов, ниже простой пример.
InterfaceA.java
1 2 3 4 5 6 |
package ua.com.prologistic.inheritance; public interface InterfaceA { public void doSomething(); } |
InterfaceB.java
1 2 3 4 5 6 |
package ua.com.prologistic.inheritance; public interface InterfaceB { public void doSomething(); } |
Обратите внимание, что в обоих интерфейсах объявлен такой же метод, а теперь посмотрим, что с этого получится:
InterfaceC.java
1 2 3 4 5 6 7 8 |
package ua.com.prologistic.inheritance; public interface InterfaceC extends InterfaceA, InterfaceB { //один и тот же метод объявлен в интерфейсах InterfaceA и InterfaceB public void doSomething(); } |
И это отличный выход, потому что интерфейсы только объявляют методы, а фактическая реализация будет сделана в конкретных классах, которые реализуют интерфейсы, так что нет никакой возможности двусмысленно трактовать множественное наследование в интерфейсе.
Теперь давайте посмотрим на код ниже:
InterfacesImpl.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
package ua.com.prologistic.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(); } } |
А как здесь использовать Композицию (Composition)?
Так что же делать, если мы хотим использовать метод methodA() класса ClassA
и метод methodB() класса ClassB
в ClassC?
Решение заключается в использовании композиции. Ниже представлена версия класса ClassC с использованием композиции:
ClassC.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
package ua.com.prologistic.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 гласит «Используйте композицию чаще наследования«. Давайте рассмотрим этот подход:
- Предположим, у нас есть суперкласс и подкласс:
ClassC.java
1 2 3 4 5 6 7 |
package ua.com.prologistic.inheritance; public class ClassC{ public void methodC(){ } } |
ClassD.java
1 2 3 4 5 6 7 8 |
package ua.com.prologistic.inheritance; public class ClassD extends ClassC{ public int test(){ return 0; } } |
Код выше компилируется и работает нормально, но что будет, если реализация класса ClassC изменяется, как показано ниже:
1 2 3 4 5 6 7 8 9 10 |
package ua.com.prologistic.inheritance; public class ClassC{ public void methodC(){ } public void test(){ } } |
Обратите внимание, что метод test() уже существует в подклассе, но тип возвращаемого отличается, теперь ClassD не будет компилироваться, и если вы используете какую-то IDE, то вам будет предложено изменить тип возвращаемого значения на тип суперкласса или подкласса.
Теперь представьте себе ситуацию, когда у нас есть несколько уровней наследования класса, однако суперкласс не контролируется нами. В этом случае мы не будет иметь выбора, кроме как изменить сигнатуру метода нашего подкласса или его имя, чтобы удалить ошибку компиляции. Также мы должны внести изменения во все места, где наш метод подкласса использовался.
Указанная проблема никогда не произойдет с композицией, поэтому это делает её более предпочтительней, чем наследование.
- Еще одна проблема с наследованием в том, что мы предоставляем все методы суперкласса клиенту, и если наш суперкласс не правильно спроектирован и есть дыры в безопасности, то даже если мы позаботимся о правильной реализации нашего подкласса, мы все равно получаем проблемы, которые достались нам от суперкласса.
Композиция помогает нам контролировать доступ к методам суперкласса, в то время как наследование не обеспечивает никакого контроля методов суперкласса. Это тоже одна из основных преимуществ композиции перед наследованием в Java. - Еще одно преимущество композиции в том, что она обеспечивает гибкость в вызове методов. Ниже приведен хороший пример использования композиции:
ClassC.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
package ua.com.prologistic.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 реализуется классом A doSomething реализуется классом B |
Эта гибкость в вызове методов не доступна в наследовании.
- При использовании композиции легко проводить модульное тестирование, потому что мы знаем, что все методы не зависят от суперкласса. В то время как при наследовании, мы в значительной степени зависим от суперкласса и не знаем какие методы будут использоваться, поэтому мы должны проверить все методы суперкласса. А это дополнительная работа, которая никому не нужна.
В идеале мы должны использовать наследование только тогда, когда «is-a» отношение справедливо для суперкласса и подкласса во всех случаях, в противном случае мы должны использовать композицию.
Следите за обновлениями на javadevblog.com — программирование на Java
В последнем примере в классе ClassC.java, скорее всего, используется агрегирование, а не композиция.