User DSergey_Kh
DSergey_Kh
12 уровень

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

Статья из группы Архив info.javarush.ru

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

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

Ромбовидная проблема

Для более легкого понимания ромбовидной проблемы, давайте предположим, что множественное наследование поддерживается в Java. В этом случае, у нас могла бы быть иерархия классов как показано на изображении ниже. Множественное наследование в Java. Композиция в сравнении с Наследованием - 1Предположим, что класс SuperClass является абстрактным и в нем объявлен некоторый метод. И конкретные классы ClassA и ClassB.

package com.journaldev.inheritance;
public abstract class SuperClass {
	public abstract void doSomething();
}
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(){		
	}
}
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.

package com.journaldev.inheritance;
public class ClassC extends ClassA, ClassB{
	public void test(){
		//calling super class method
		doSomething();
	}
}
Обратите внимание, что метод test() вызывает метод суперкласса doSomething(). Это приводит к неоднозначности, поскольку компилятор не знает, какой метод суперкласса выполнять. Это есть диаграмма классов ромбовидной формы, которая называется ромбовидная проблема. Это есть главная причина, по которой в Java не поддерживается множественное наследование. Заметьте, что вышеупомянутая проблема с многократным наследованием классов может произойти только с тремя классами, у которых есть хоть один общий метод.

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

В Java множественное наследование не поддерживается в классах, но оно поддерживается в интерфейсах. И один интерфейс может расширять множество других интерфейсов. Ниже представлен простой пример.

package com.journaldev.inheritance;
public interface InterfaceA {
	public void doSomething();
}
package com.journaldev.inheritance;
public interface InterfaceB {
	public void doSomething();
}
Обратите внимание, что оба интерфейса объявляют один и тот же самый метод. Теперь мы можем создать интерфейс, расширяющий оба эти интерфейса, как представлено в примере ниже.

package com.journaldev.inheritance;
public interface InterfaceC extends InterfaceA, InterfaceB {
	//same method is declared in InterfaceA and InterfaceB both
	public void doSomething();	
}
Это отлично работает, потому что интерфейсы только объявляют методы, а реализация будет выполнена в классах, наследующих интерфейс. Таким образом нет никакой возможности получить неоднозначность во множественном наследовании интерфейсов.

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. Что делать, если мы хотим использовать функцию methodA(), класса ClassA и функцию methodB(), класса ClassB в классе ClassC? Решение находится в использовании композиции. Ниже представлена версия класса ClassC, которая использует композицию, чтобы определить оба метода классов и метод doSomething(), одного из объектов.

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. Предположим, что у нас есть суперкласс и класс, расширяющий его:

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

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

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

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

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

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

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

  3. Еще одно преимущество композиции – то, что она обеспечивает гибкости в вызове методов. Наша реализация класса ClassC, представленная выше не оптимальна и обеспечивает связывание времени компиляции с методом, который будет вызываться. С минимальными изменениями мы можем сделать вызов метода гибким и динамичным.

    
    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 implementation of A
    doSomething implementation of B
    

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

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

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

Комментарии (3)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
🦔 Виктор Уровень 20, Москва, Россия Expert
22 декабря 2020
TL;DR В джаве нет множественного наследования, но зато оно есть в интерфейсах. p.s. Ранова-то меня сюда запульнуло... -- tlgrm: @LetsCodeIt | @SefoNotasi
Temnota Уровень 19, Минск, Беларусь
26 апреля 2020
прикольно, что тут говорилось, что множественного наследования в Java нет)