Некоторое время назад я написал несколько постов о наследовании , интерфейсе и композиции в 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-программирования — «отдавать предпочтение композиции поверх интерфейсов», мы рассмотрим некоторые аспекты, поддерживающие этот подход.
- Предположим, у нас есть суперкласс и подкласс следующим образом:
ClassC.java
1234567package
com.journaldev.inheritance;
public
class
ClassC{
public
void
methodC(){
}
}
ClassD.java
12345678package
com.journaldev.inheritance;
public
class
ClassD
extends
ClassC{
public
int
test(){
return
0
;
}
}
Приведенный выше код компилируется и работает нормально, но что, если реализация ClassC будет изменена, как показано ниже:
ClassC.java
01020304050607080910package
com.journaldev.inheritance;
public
class
ClassC{
public
void
methodC(){
}
public
void
test(){
}
}
Обратите внимание, что метод test () уже существует в подклассе, но тип возвращаемого значения отличается, теперь ClassD не будет компилироваться, и если вы используете какую-либо IDE, он предложит вам изменить тип возвращаемого значения либо в суперклассе, либо в подклассе.
Теперь представьте ситуацию, когда у нас есть многоуровневое наследование классов, и суперкласс не контролируется нами, у нас не будет другого выбора, кроме как изменить сигнатуру метода подкласса или его имя, чтобы удалить ошибку компиляции, также нам придется вносить изменения во все места, где вызывался наш метод подкласса, поэтому наследование делает наш код хрупким.
Вышеупомянутая проблема никогда не возникнет с композицией, и это делает ее более благоприятной по сравнению с наследованием.
- Другая проблема с наследованием состоит в том, что мы открываем все методы суперкласса клиенту, и если наш суперкласс не спроектирован должным образом и есть дыры в безопасности, то даже несмотря на то, что мы полностью заботимся о реализации нашего класса, мы страдаем от плохой реализации суперкласс.
Композиция помогает нам в обеспечении контролируемого доступа к методам суперкласса, тогда как наследование не обеспечивает какого-либо контроля над методами суперкласса, это также является одним из основных преимуществ композиции по сравнению с наследованием. - Другое преимущество композиции заключается в том, что она обеспечивает гибкость при вызове методов. Наша реализация
ClassC
выше не является оптимальной и обеспечивает привязку времени компиляции с методом, который будет вызван, с минимальными изменениями мы можем сделать вызов метода гибким и сделать его динамичным.
ClassC.java
010203040506070809101112131415161718192021package
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();
}
}
Вывод вышеуказанной программы:
12doSomething implementation of A
doSomething implementation of B
Такая гибкость при вызове метода недоступна при наследовании и повышает эффективность, чтобы отдавать предпочтение композиции по сравнению с наследованием.
- Модульное тестирование легко по составу, потому что мы знаем, какие все методы мы используем из суперкласса, и мы можем макетировать его для тестирования, тогда как в наследовании мы сильно зависим от суперкласса и не знаем, какие будут использоваться все методы суперкласса, поэтому нам нужно протестировать все методы суперкласса, это дополнительная работа, и мы должны делать это без необходимости из-за наследования.
В идеале мы должны использовать наследование только тогда, когда отношение « is-a » сохраняется для суперкласса и подкласса во всех случаях, иначе мы должны продолжить композицию.