Сегодня мы рассмотрим Множественное Наследование в Java. Некоторое время назад я написал несколько постов о наследовании, интерфейсах и композиции в Java. В этом посте мы рассмотрим множественное наследование в Java, а затем сравним его с композицией и наследованием.
Множественное Наследование в Java
Множественное наследование в Java – это возможность создания одного класса с несколькими суперклассами. В отличие от некоторых других популярных объектно-ориентированных языков программирования, таких как C++, Java не предоставляет поддержки для множественного наследования в классах. Java не поддерживает множественное наследование в классах, потому что это может привести к проблеме ромба, и вместо того, чтобы предоставлять сложные способы ее решения, существуют лучшие способы достижения того же результата, что и множественное наследование.
Проблема алмаза в Java
Чтобы легче понять проблему алмаза, давайте предположим, что в Java поддерживается множественное наследование. В таком случае мы могли бы иметь иерархию классов, как на изображении ниже. Допустим, что SuperClass – это абстрактный класс, объявляющий некоторый метод, и ClassA, ClassB – конкретные классы.
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("doSomething implementation of A");
}
//Собственный метод ClassA
public void methodA(){
}
}
ClassB.java
package com.journaldev.inheritance;
public class ClassB extends SuperClass{
@Override
public void doSomething(){
System.out.println("doSomething implementation of B");
}
//Собственный метод ClassB
public void methodB(){
}
}
Теперь предположим, что реализация ClassC будет что-то вроде приведенного ниже и класс наследует как ClassA, так и ClassB. ClassC.java
package com.journaldev.inheritance;
// это всего лишь предположение для объяснения проблемы алмаза
//этот код не будет компилироваться
public class ClassC extends ClassA, ClassB{
public void test(){
//вызов метода суперкласса
doSomething();
}
}
Обратите внимание, что метод test()
вызывает метод суперкласса doSomething()
. Это приводит к неоднозначности, поскольку компилятор не знает, метод какого суперкласса выполнить. Из-за классной диаграммы в форме бриллианта это называется проблемой бриллианта в Java. Проблема бриллианта в Java – основная причина того, почему в Java не поддерживается множественное наследование в классах. Обратите внимание, что вышеописанная проблема с множественным наследованием классов может возникнуть также с тремя классами, у которых у всех есть хотя бы один общий метод.
Множественное наследование в интерфейсах Java
Вы, возможно, заметили, что я всегда говорю, что множественное наследование не поддерживается в классах, но поддерживается в интерфейсах. Один интерфейс может расширять несколько интерфейсов, вот простой пример. InterfaceA.java
package com.journaldev.inheritance;
public interface InterfaceA {
public void doSomething();
}
InterfaceB.java
package com.journaldev.inheritance;
public interface InterfaceB {
public void doSomething();
}
Обратите внимание, что оба интерфейса объявляют одинаковый метод, теперь мы можем иметь интерфейс, расширяющий оба эти интерфейса, как показано ниже. InterfaceC.java
package com.journaldev.inheritance;
public interface InterfaceC extends InterfaceA, InterfaceB {
//одинаковый метод объявлен в InterfaceA и InterfaceB
public void doSomething();
}
Это вполне нормально, потому что интерфейсы только объявляют методы, и фактическая реализация будет выполнена конкретными классами, реализующими интерфейсы. Поэтому нет возможности какого-либо неоднозначия при множественном наследовании в интерфейсах Java. Вот почему класс Java может реализовывать несколько интерфейсов, что-то вроде приведенного ниже примера. InterfacesImpl.java
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();
// все вызовы методов ниже идут к той же конкретной реализации
objA.doSomething();
objB.doSomething();
objC.doSomething();
}
}
Заметили ли вы, что каждый раз, когда я переопределяю метод суперкласса или реализую метод интерфейса, я использую аннотацию @Override? Аннотация Override – одна из трех встроенных аннотаций Java, и мы должны всегда использовать аннотацию override при переопределении любого метода.
Композиция на выручку
И что делать, если мы хотим использовать функцию methodA()
из ClassA
и функцию methodB()
из ClassB
в ClassC
. Решение заключается в использовании композиции. Вот переработанная версия ClassC, использующая композицию для использования методов обоих классов и также использующая метод doSomething() одного из объектов. 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();
}
}
Композиция против Наследования
Одна из лучших практик программирования на Java – “предпочитать композицию перед наследованием”. Мы рассмотрим некоторые аспекты, поддерживающие этот подход.
-
Предположим, у нас есть суперкласс и подкласс следующим образом:
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; } }
Приведенный выше код компилируется и работает нормально, но что, если реализация ClassC изменится следующим образом:
ClassC.java
package com.journaldev.inheritance; public class ClassC{ public void methodC(){ } public void test(){ } }
Обратите внимание, что метод
test()
уже существует в подклассе, но возвращаемый тип отличается. Теперь ClassD не скомпилируется, и если вы используете любую среду разработки, она предложит вам изменить тип возвращаемого значения либо в суперклассе, либо в подклассе. Теперь представьте ситуацию, когда у нас есть несколько уровней наследования классов, и суперкласс не контролируется нами. У нас не останется выбора, кроме как изменить сигнатуру метода подкласса или его имя, чтобы устранить ошибку компиляции. Кроме того, нам придется внести изменения во всех местах, где вызывался метод нашего подкласса, таким образом, наследование делает наш код хрупким. Проблема, описанная выше, никогда не возникнет при использовании композиции, что делает ее более предпочтительной перед наследованием. -
Еще одна проблема с наследованием заключается в том, что мы предоставляем клиенту доступ ко всем методам суперкласса, и если наш суперкласс не правильно спроектирован и имеет уязвимости, то даже если мы полностью заботимся о реализации нашего класса, мы подвергаемся воздействию плохой реализации суперкласса. Композиция помогает нам предоставить контролируемый доступ к методам суперкласса, тогда как наследование не предоставляет никакого контроля над методами суперкласса, и это также одно из основных преимуществ композиции по сравнению с наследованием.
-
Еще одно преимущество композиции заключается в том, что она обеспечивает гибкость при вызове методов. Наша вышеуказанная реализация
ClassC
не является оптимальной и обеспечивает связывание с методом на этапе компиляции, с минимальными изменениями мы можем сделать вызов метода гибким и динамическим.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(); } }
Результат выполнения вышеуказанной программы:
Реализация doSomething класса A Реализация doSomething класса B
Такая гибкость при вызове методов недоступна при использовании наследования и способствует применению композиции вместо наследования как лучшей практики.
-
Модульное тестирование легко в композиции, потому что мы знаем, какие методы мы используем из суперкласса, и можем их имитировать для тестирования. В то время как в случае наследования мы сильно зависим от суперкласса и не знаем, какие методы из суперкласса будут использованы, поэтому нам нужно тестировать все методы суперкласса. Это лишняя работа, которую мы делаем из-за наследования.
Вот и всё по поводу множественного наследования в Java и краткого обзора композиции.
Source:
https://www.digitalocean.com/community/tutorials/multiple-inheritance-in-java