Почему в java нет множественного наследования
Перейти к содержимому

Почему в java нет множественного наследования

  • автор:

Чем в java заменить множественное наследование?

Хочу реализовать приблизительно следующую архитектуру:

Пояснение:

  • Base — Что-то вроде сущности-хранилища, в котором хранятся какие-то базовые объекты;
  • ChildN — потомки, реализующие свой спектр действий над данными, которые хранятся в классе Base . По сути являются интерфейсами, т.к. никакого состояния не хранят;
  • static Mixin — класс, объединяющий в себе все методы из ChildN . Коллизий между именами методов нет. Все его методы (т.е. методы, унаследованные от ChildN ) должны быть статическими, вызываться без инстанцирования.

В python эта задача легко бы решилась множественным наследованием, в java такого нет. Как быть?

Примерный псевдокод того, что хочу реализовать:

class Base < static protected final String someString = "01"; >class Child1 extends Base < static public char get_0() < return someString.charAt(0); >> class Child2 extends Base < static public char get_1() < return someString.charAt(1); >> class MixIn extends Child1, Child2 < // Так сделать нельзя >public class Main < public static void main(String[] args) < System.out.println(MixIn.get_0()); // Итоговое использование должно быть таким System.out.println(MixIn.get_1()); >> 

Отслеживать
задан 11 дек 2020 в 9:21
Mikhail Murugov Mikhail Murugov
5,426 1 1 золотой знак 16 16 серебряных знаков 37 37 бронзовых знаков

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

11 дек 2020 в 9:27

у всех Ваших классов будет один общий предок. И тут возникает целый спектр разных проблем. Поэтому в Java и решили отказаться от этого. Но можно делать множественное «наследование» от интерфейсов. А у интерфейсов теперь есть «имплементация методов»:)

11 дек 2020 в 9:28

Если Base — хранилище, используйте композицию. Использование наследования здесь будет семантически неверно, т.к. наследование классов, выполняющих операции над данными, от класса Base , не представляет отношение IS-A

11 дек 2020 в 9:29

Все было понятно пока не появились статические методы. Вы просто так добавили static? Для статических методов наследование здесь имеет мало смысла. Почему нельзя написать Child1.get_0() и Child2.get_1() если методы не переопределяются все равно? Может доработаете пример чтобы было понятнее что именно Вам нужно?

11 дек 2020 в 9:30

Возможно, паттерн стратегия подойдет. Классы ChildN выполняют только действия, поэтому их вполне можно заменить на стратегии.

11 дек 2020 в 9:32

1 ответ 1

Сортировка: Сброс на вариант по умолчанию

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

Использование интерфейсов

Вы можете сделать child-ы интерфейсами. В Java нет множественно наследования, зато можно имплементировать несколько интерфейсов. Чтобы сделать реализацию методов прямо в интерфейсах (чтобы они не требовали реализации в реализующем их классе), можно использовать ключевое слово default :

class Base < static public final String str = "01"; >interface Child1 < default char get0() < return Base.str.charAt(0); >> interface Child2 < default char get1() < return Base.str.charAt(1); >> class MixIn implements Child1, Child2 < >class Main < public static void main(String[] args) < MixIn mixIn = new MixIn(); System.out.println(mixIn.get0()); System.out.println(mixIn.get1()); >> 

Отказ от разделения функционала

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

class Base < private final String str = "01"; public String getStr() < return str; >> class MixIn < private Base base; public MixIn(Base base) < this.base = base; >public char get0() < return base.getStr().charAt(0); >public char get1() < return base.getStr().charAt(1); >> public class Main < public static void main(String[] args) < MixIn mixIn = new MixIn(new Base()); System.out.println(mixIn.get0()); System.out.println(mixIn.get1()); >> 

Использование статических внутренних классов

Вы говорите, что вам нужна логическая группировка. В Java существует конструкция, которая называется статический внутренний или вложенный класс. Внутри класса создаётся ещё один класс, который отмечается static . Таким образом вы можете достичь логической группировки. Пример:

class Base < static public final String someString = "01"; >class MixIn < public static class Child1 < static char get1() < return Base.someString.charAt(1); >> public static class Child2 < static char get0() < return Base.someString.charAt(0); >> > 

Видите, я создал общий класс MixIn, а внутри него два статических класса. Вызываются они вот так:

class Main < public static void main(String[] args) < System.out.println(MixIn.Child1.get1()); System.out.println(MixIn.Child2.get0()); >> 

Как видите, никакого инстанцирование нет, а классы/методы логически сгруппированы.

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

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

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

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

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

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

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

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

Организация множественного наследования в Java

В отличие от своего объектно-ориентированного предшественника — языка С++, в языке Java не поддерживается множественное наследование (МН). Но рано или поздно любой программист, использующий парадигму объектно-ориентированного программирования (ООП), сталкивается с необходимостью опираться на методы от разных родительских классов. А опыт применения МН в С++ показал, что это приводит к большему количеству проблем, чем ожидалось. Поэтому в рамках построения парадигмы ООП создатели JAVA подошли с других позиций. Так как же этот вопрос был решён в JAVA?

Для начала рассмотрим как исторически развивался вопрос о проблемах наследования на примере С++.

Множественное наследование позволяет одному дочернему классу иметь несколько родителей. Предположим, что мы хотим написать программу для отслеживания работы учителей. Учитель — это Human. Тем не менее, он также является Сотрудником (Employee).

https://ravesli.com/wp-content/uploads/2018/09/diagram-lesson-161.jpg

Множественное наследование может быть использовано для создания класса Teacher, который будет наследовать свойства как Human, так и Employee. Для использования множественного наследования нужно просто указать через запятую тип наследования и второй родительский класс:

#include class Human < private: std::string m_name; int m_age; public: Human(std::string name, int age) : m_name(name), m_age(age) < >std::string getName() < return m_name; >int getAge() < return m_age; >>; class Employee < private: std::string m_employer; double m_wage; public: Employee(std::string employer, double wage) : m_employer(employer), m_wage(wage) < >std::string getEmployer() < return m_employer; >double getWage() < return m_wage; >>; // Класс Teacher открыто наследует свойства классов Human и Employee class Teacher: public Human, public Employee < private: int m_teachesGrade; public: Teacher(std::string name, int age, std::string employer, double wage, int teachesGrade) : Human(name, age), Employee(employer, wage), m_teachesGrade(teachesGrade) < >>;

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

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

#include class USBDevice < private: long m_id; public: USBDevice(long id) : m_id(id) < >long getID() < return m_id; >>; class NetworkDevice < private: long m_id; public: NetworkDevice(long id) : m_id(id) < >long getID() < return m_id; >>; class WirelessAdapter: public USBDevice, public NetworkDevice < public: WirelessAdapter(long usbId, long networkId) : USBDevice(usbId), NetworkDevice(networkId) < >>; int main() < WirelessAdapter c54G(6334, 292651); std::cout 

При компиляции c54G.getID() компилятор смотрит, есть ли у WirelessAdapter метод getID(). Этого метода у него нет, поэтому компилятор двигается по цепочке наследования вверх и смотрит есть ли этот метод в каком-либо из родительских классов. И здесь возникает проблема — getID() есть как у USBDevice, так и у NetworkDevice. Следовательно, вызов этого метода приведёт к неоднозначности и мы получим ошибку, так как компилятор не будет знать какую версию getID() вызывать.

Тем не менее, есть способ обойти эту проблему. Мы можем явно указать, какую версию getID() следует вызывать:

int main()

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

Во-вторых, более серьёзной проблемой является «алмаз смерти» (или ещё «алмаз обреченности», или «ромб»). Это ситуация, когда один класс имеет 2 родительских класса, каждый из которых, в свою очередь, наследует свойства одного и того же родительского класса. Иллюстративно мы получаем форму алмаза.

Например, рассмотрим следующие классы:

class PoweredDevice < >; class Scanner: public PoweredDevice < >; class Printer: public PoweredDevice < >; class Copier: public Scanner, public Printer < >;

Сканеры и принтеры — это устройства, которые получают питание от розетки, поэтому они наследуют свойства PoweredDevice. Однако копировальный аппарат включает в себя функции как сканеров, так и принтеров.

https://ravesli.com/wp-content/uploads/2018/09/diamond-of-death-cpp.jpg

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

Так стоит ли использовать множественное наследование?

Большинство задач, решаемых с помощью множественного наследования, могут быть решены и с использованием одиночного наследования. Многие объектно-ориентированные языки программирования (например: Smalltalk, PHP) даже не поддерживают множественное наследование. Многие, относительно современные языки, такие как Java и C#, ограничивают классы одиночным наследованием, но допускают множественное наследование интерфейсов (об этом поговорим позже). Суть идеи, запрещающей множественное наследование в этих языках, заключается в том, что это лишняя сложность, которая порождает больше проблем, чем удобств.

Многие опытные программисты считают, что множественное наследование в C++ следует избегать любой ценой из-за потенциальных проблем, которые могут возникнуть. Однако всё же остаётся вероятность, когда множественное наследование будет лучшим решением, нежели придумывание двухуровневых костылей.

Правило С++: Используйте множественное наследование только в крайних случаях, когда задачу нельзя решить одиночным наследованием, либо другим альтернативным способом (без изобретения «велосипедов»).

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

Другими словами, для каждого класса в Java может существовать только один родительский класс. Тем не менее в каждом классе можно реализовать произвольное количество интерфейсов.

При этом данный класс будет соответствовать типам всех тех интерфейсов, которые в нем реализованы.

Как видите, с помощью интерфейсов создаются новые типы объектов без их реализации.

Как известно, в абстрактном классе допускается реализация некоторых методов, не объявленных абстрактными. В отличие от них, интерфейсы — это чистой воды шаблоны. С помощью интерфейса можно только определить функциональность, но не реализовать ее (исключения составляют дефолтные методы, которые появились в Java 8).

Чтобы определить интерфейс, используется ключевое слово interface. Например:

interface Printable

Нужно обратить внимание, что интерфейсы — это не классы, хотя и очень похожи на них.

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

Данный интерфейс называется Printable. Интерфейс может определять константы и методы, которые могут иметь, а могут и не иметь реализации. Методы без реализации похожи на абстрактные методы абстрактных классов. Так, в данном случае объявлен один метод, который не имеет реализации.

Все методы интерфейса не имеют модификаторов доступа, но фактически по умолчанию доступ public, так как цель интерфейса — определение функционала для реализации его классом. Поэтому весь функционал должен быть открыт для реализации.

public class Program < public static void main(String[] args) < Book b1 = new Book("Java. Complete Referense.", "H. Shildt"); b1.print(); >> interface Printable < void print(); >class Book implements Printable < String name; String author; Book(String name, String author)< this.name = name; this.author = author; >public void print() < System.out.printf("%s (%s) \n", name, author); >>

В данном случае класс Book реализует интерфейс Printable. При этом надо учитывать, что если класс применяет интерфейс, то он должен реализовать все методы интерфейса, как в случае выше реализован метод print. Потом в методе main мы можем объект класса Book и вызвать его метод print. Если класс не реализует какие-то методы интерфейса, то такой класс должен быть определен как абстрактный, а его неабстрактные классы-наследники затем должны будут эти методы реализовать.

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

Printable pr = new Printable(); pr.print();

Одним из преимуществ использования интерфейсов является то, что они позволяют добавить в приложение гибкости. Например, в дополнение к классу Book определим еще один класс, который будет реализовывать интерфейс Printable:

class Journal implements Printable < private String name; String getName()< return name; >Journal(String name) < this.name = name; >public void print() < System.out.println(name); >>

Класс Book и класс Journal связаны тем, что они реализуют интерфейс Printable. Поэтому мы динамически в программе можем создавать объекты Printable как экземпляры обоих классов:

public class Program < public static void main(String[] args) < Printable printable = new Book("Java. Complete Reference", "H. Shildt"); printable.print(); // Java. Complete Reference (H. Shildt) printable = new Journal("Foreign Policy"); printable.print(); // Foreign Policy >> interface Printable < void print(); >class Book implements Printable < String name; String author; Book(String name, String author) < this.name = name; this.author = author; >public void print() < System.out.printf("%s (%s) \n", name, author); >> class Journal implements Printable < private String name; String getName()< return name; >Journal(String name) < this.name = name; >public void print() < System.out.println(name); >>
Интерфейсы в преобразованиях типов

Интерфейс — это ссылочный тип. Например, так как класс Journal реализует интерфейс Printable, то переменная типа Printable может хранить ссылку на объект типа Journal:

Printable p =new Journal("Foreign Affairs"); p.print(); // Интерфейс не имеет метода getName, необходимо явное приведение String name = ((Journal)p).getName(); System.out.println(name);

И если мы хотим обратиться к методам класса Journal, которые определены не в интерфейсе Printable, а в самом классе Journal, то нам надо явным образом выполнить преобразование типов:

((Journal)p).getName();
Методы по умолчанию

Ранее до JDK 8 при реализации интерфейса мы должны были обязательно реализовать все его методы в классе. А сам интерфейс мог содержать только определения методов без конкретной реализации. В JDK 8 была добавлена такая функциональность как методы по умолчанию. И теперь интерфейсы кроме определения методов могут иметь их реализацию по умолчанию, которая используется, если класс, реализующий данный интерфейс, не реализует метод. Например, создадим метод по умолчанию в интерфейсе Printable:

interface Printable < default void print() < System.out.println("Undefined printable"); >>

Метод по умолчанию — это обычный метод без модификаторов, который помечается ключевым словом default. Затем в классе Journal нам необязательно этот метод реализовывать, хотя мы можем его и переопределить:

class Journal implements Printable < private String name; String getName() < return name; >Journal(String name) < this.name = name; >>
Статические методы

Начиная с JDK 8 в интерфейсах доступны статические методы — они аналогичны методам класса:

interface Printable < void print(); static void read() < System.out.println("Read printable"); >>

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

public static void main(String[] args)

Приватные методы

По умолчанию все методы в интерфейсе фактически имеют модификатор public. Однако начиная с Java 9 мы также можем определять в интерфейсе методы с модификатором private. Они могут быть статическими и нестатическими, но они не могут иметь реализации по умолчанию.

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

public class Program < public static void main(String[] args) < Calculable c = new Calculation(); System.out.println(c.sum(1, 2)); System.out.println(c.sum(1, 2, 4)); >> class Calculation implements Calculable < >interface Calculable < default int sum(int a, int b) < return sumAll(a, b); >default int sum(int a, int b, int c) < return sumAll(a, b, c); >private int sumAll(int. values) < int result = 0; for(int n : values)< result += n; >return result; > >
Константы в интерфейсах

Кроме методов в интерфейсах могут быть определены статические константы:

interface Stateable

Хотя такие константы также не имеют модификаторов, но по умолчанию они имеют модификатор доступа public static final , и поэтому их значение доступно из любого места программы.

public class Program < public static void main(String[] args) < WaterPipe pipe = new WaterPipe(); pipe.printState(1); >> class WaterPipe implements Stateable < public void printState(int n) < if(n==OPEN) System.out.println("Water is opened"); else if(n==CLOSED) System.out.println("Water is closed"); else System.out.println("State is invalid"); >> interface Stateable

Множественная реализация интерфейсов

Если нам надо применить в классе несколько интерфейсов, то они все перечисляются через запятую после слова implements:

interface Printable < // методы интерфейса >interface Searchable < // методы интерфейса >class Book implements Printable, Searchable < // реализация класса >
Наследование интерфейсов

Интерфейсы, как и классы, могут наследоваться:

interface BookPrintable extends Printable

При применении этого интерфейса класс Book должен будет реализовать как методы интерфейса BookPrintable, так и методы базового интерфейса Printable.

Вложенные интерфейсы

Как и классы, интерфейсы могут быть вложенными, то есть могут быть определены в классах или других интерфейсах. Например:

class Printer < interface Printable < void print(); >>

При применении такого интерфейса нам надо указывать его полное имя вместе с именем класса:

public class Journal implements Printer.Printable < String name; Journal(String name) < this.name = name; >public void print() < System.out.println(name); >>

Использование интерфейса будет аналогично предыдущим случаям:

Printer.Printable p =new Journal("Foreign Affairs"); p.print();
Интерфейсы как параметры и результаты методов

И также как и в случае с классами, интерфейсы могут использоваться в качестве типа параметров метода или в качестве возвращаемого типа:

public class Program < public static void main(String[] args) < Printable printable = createPrintable("Foreign Affairs",false); printable.print(); read(new Book("Java for impatiens", "Cay Horstmann")); read(new Journal("Java Daily News")); >static void read(Printable p) < p.print(); >static Printable createPrintable(String name, boolean option) < if(option) return new Book(name, "Undefined"); else return new Journal(name); >> interface Printable < void print(); >class Book implements Printable < String name; String author; Book(String name, String author) < this.name = name; this.author = author; >public void print() < System.out.printf("%s (%s) \n", name, author); >> class Journal implements Printable < private String name; String getName()< return name; >Journal(String name) < this.name = name; >public void print() < System.out.println(name); >>

Метод read() в качестве параметра принимает любой объект, реализующий интерфейс Printable, поэтому в этот метод мы можем передать как объект Book, так и объект Journal.

Метод createPrintable() возвращает объект, реализующий Printable, поэтому также мы можем возвратить как объект Book, так и Journal.

Foreign Affairs Java for inpatients (Cay Horstmann) Java Daily News

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

https://javadevblog.com/wp-content/uploads/2015/05/multiinh.png

Давайте создадим абстрактный суперкласс SuperClass с методом doSomething(), а также два класса ClassA, ClassB

public abstract class SuperClass < public abstract void doSomething(); >public class ClassA extends SuperClass < @Override public void doSomething()< System.out.println("doSomething реализуется в классе A"); >//Собственный метод класса ClassA public void methodA() < >> public class ClassB extends SuperClass < @Override public void doSomething()< System.out.println("doSomething реализуется классом B"); >//Свой метод класса ClassB public void methodB() < >>

А теперь давайте создадим класс ClassC, который наследует классы ClassA и ClassB

public class ClassC extends ClassA, ClassB < public void test() < //вызываем метод суперкласса doSomething(); >>

Обратите внимание, что метод test() вызывает метод суперкласса doSomething()

Это приводит к неопределенности: компилятор не знает, какой метод суперкласса выполнить из-за ромбовидной формы (выше на диаграмме классов). Это и называют проблемой ромба — и это основная причина почему Java не поддерживает множественное наследование классов.

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

МН не поддерживается в классах, но оно поддерживается в интерфейсах и единый интерфейс может наследовать несколько интерфейсов, ниже простой пример.

public interface InterfaceA < public void doSomething(); >public interface InterfaceB

Обратите внимание, что в обоих интерфейсах объявлен такой же метод, а теперь посмотрим, что с этого получится:

public interface InterfaceC extends InterfaceA, InterfaceB < //один и тот же метод объявлен в интерфейсах InterfaceA и InterfaceB public void doSomething(); >

И это отличный выход, потому что интерфейсы только объявляют методы, а фактическая реализация будет сделана в конкретных классах, которые реализуют интерфейсы, так что нет никакой возможности двусмысленно трактовать множественное наследование в интерфейсе. Все усложняется, если некий класс реализует более одного (скажем, два) интерфейса, а они реализуют один и тот же самый метод по умолчанию. Какой из методов унаследует класс? Ответ — никакой. В таком случае класс должен реализовать метод самостоятельно (напрямую, либо унаследовав его от другого класса).

Теперь давайте посмотрим на код ниже:

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 с использованием композиции:

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 гласит «Используйте композицию чаще наследования». Давайте рассмотрим этот подход:

Предположим, у нас есть суперкласс и подкласс:

public class ClassC < public void methodC()< >> public class ClassD extends ClassC < public int test()< return 0; >>

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

public class ClassC < public void methodC()< >public void test() < >>

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

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

Указанная проблема никогда не произойдет с композицией, поэтому это делает её предпочтительней, чем наследование.

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

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

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

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

Эта гибкость в вызове методов не доступна в наследовании.

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

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

Константин Кишкин, после окончания базового курса, июль 2020

  • Мэтт Зандстра – «Объекты, шаблоны и методики программирования», 4-е издание, Издательский дом «Вильямс», 2015
  • https://ravesli.com/urok-161-mnozhestvennoe-nasledovanie/
  • https://habr.com/ru/post/482498/
  • https://javadevblog.com/mnozhestvennoe-nasledovanie-v-java-i-kompozitsiya-vs-nasledovaniya.html
  • https://metanit.com/java/tutorial/3.7.php

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

— Привет, Амиго! Наконец-то мы добрались до очень интересной темы. Сегодня я расскажу тебе про множественное наследование . На самом деле множественное наследование очень интересный и мощный инструмент. И если бы не некоторые проблемы, то в Java было бы множественное наследование классов. Но т.к. его нет, придется довольствоваться множественным наследованием интерфейсов . Что тоже не мало.

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

Представь, что ты пишешь компьютерную игру. И ее герои – твои объекты – должны демонстрировать очень сложное поведение: ходить по карте, собирать предметы, выполнять квесты, общаться с другими героями, кого-то убивать, кого-то спасать. Допустим, ты смог разделить все объекты на 20 категорий. Это значит, что если тебе повезет, ты можешь обойтись всего 20-ю классами, для их описания. А теперь вопрос на засыпку: сколько всего уникальных видов взаимодействия у этих объектов. Объект каждого типа может иметь уникальные взаимодействия с 20-ю видами других объектов (себе подобных тоже считаем). Т.е. всего нужно запрограммировать 20 на 20 – 400 взаимодействий! А если уникальных видов объектов будет не 20, а 100, количество взаимодействий может достигнуть 10,000!

— Ничего себе! Теперь понимаю, почему программирование такая непростая работа.

— Она простая. Благодаря многим абстракциям. И в не последнюю очередь – множественному наследованию интерфейсов.

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

Когда пишется большая программа, обычно с этого сразу и начинают:

1) Определяют все существующие способности/роли.

2) Затем описывают взаимодействие между этими ролями.

3) А потом просто наделяют все классы их ролями.

— Конечно. Давай рассмотрим роли, на основе героев мультика «Том и Джерри».

interface Moveable <>
interface Eatable <>
interface Eat <>
class Tom extends Cat implements Moveable, Eatable, Eat <>
class Jerry extends Mouse implements Moveable, Eatable <>
class Killer extends Dog implements Moveable, Eat <>

Зная всего эти три роли (интерфейса) можно написать программу и описать корректное взаимодействие этих ролей. Например, объект будет гнаться (посредством интерфейса Moveable) за тем, «кого ты можешь съесть» и убегать от того, «кто может съесть тебя». И все это без знаний о конкретных объектах. Если в программу добавить еще объектов (классов), но оставить эти роли, она будет прекрасно работать – управлять поведением своих объектов.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *