Множественное наследование в Java. Сравнение композиции и наследования
Некоторое время назад я написал несколько постов о наследовании, интерфейсах и композиции в Java. В этой статье мы рассмотрим множественное наследование, а затем узнаем о преимуществах композиции перед наследованием.
Множественное наследование в Java
Множественное наследование – способность создавать классы с множеством классов-родителей. В отличии от других популярных объектно-ориентированных языков, вроде С++, язык Java не поддерживает множественное наследование классов. Не поддерживает он его из-за вероятности столкнуться с «проблемой алмаза» и вместо этого предпочитает обеспечивать некий комплексный подход для его решения, используя лучшие варианты из тех, которыми мы можем достичь аналогичный результат наследования.
«Проблема Алмаза»
Для более простого понимания проблемы алмаза допустим, что множественное наследование поддерживается в Java. В этом случае, мы можем получить классы с иерархией показанной на рисунке ниже. Предположим ,что SuperClass — это абстрактный класс, описывающий некоторый метод, а классы ClassA и ClassB — реальные классы. SuperClass.java
package com.journaldev.inheritance; public abstract class SuperClass
package com.journaldev.inheritance; public class ClassA extends SuperClass < @Override public void doSomething()< System.out.println("Какая-то реализация класса A"); >//собственный метод класса ClassA public void methodA() < >>
Теперь, давайте предположим, что класс ClassC наследуется от ClassA и ClassB одновременно, и при этом имеет следующую реализацию:
package com.journaldev.inheritance; public class ClassC extends ClassA, ClassB < public void test()< //вызов метода родительского класса doSomething(); >>
Заметьте, что метод test() вызывает метод doSomething() родительского класса, что приведет к неопределенности, так как компилятор не знает о том, метод какого именно суперкласса должен быть вызван. Благодаря очертаниям диаграммы наследования классов в этой ситуации, напоминающим очертания граненого алмаза проблема получила название «проблема Алмаза». Это и есть основная причина, почему в Java нет поддержки множественного наследования классов. Отметим, что указанная проблема с множественным наследованием классов также может возникнуть с тремя классами, имеющими как минимум один общий метод.
Множественное наследование и интерфейсы
Вы могли обратить внимание, что я всегда говорю: «множественное наследование не поддерживается между классами», но оно поддерживается между интерфейсами. Ниже показан простой пример: InterfaceA.java
package com.journaldev.inheritance; public interface InterfaceA
package com.journaldev.inheritance; public interface InterfaceB
Заметьте, что оба интерфейса, имеют метод с одинаковым названием. Теперь, допустим, у нас есть интерфейс, унаследованный от обоих интерфейсов. InterfaceC.java
package com.journaldev.inheritance; public interface InterfaceC extends InterfaceA, InterfaceB < //метод, с тем же названием описан в InterfaceA и InterfaceB public void doSomething();
Здесь, все идеально, поскольку интерфейсы – это только резервирование/описание метода, а реализация самого метода будет в конкретном классе, реализующем эти интерфейсы, таким образом нет никакой возможности столкнуться с неопределенностью при множественном наследовании интерфейсов. Вот почему, классы в Java могут наследоваться от нескольких интерфейсов. Покажем на примере ниже. InterfacesImpl.java
package com.journaldev.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(); >>
Возможно, вы обратили внимание, что я каждый раз, когда я переопределяю метод описанный в суперклассе или в интерфейсе я использую аннотацию @Override. Это одна из трех встроенных Java-аннотаций и вам следует всегда использовать ее при переопределении методов.
Композиция как спасение
Так что же делать, если мы хотим использовать функцию methodA() класса ClassA и methodB() класса ClassB в ClassС ? Решением этого может стать композиция – переписанная версия ClassC реализующая оба метода классов ClassA и ClassB , также имеющая реализацию 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(); >>
Композиция или наследование?
package com.journaldev.inheritance; public class ClassC < public void methodC()< >>
package com.journaldev.inheritance; public class ClassD extends ClassC < public int test()< return 0; >>
package com.journaldev.inheritance; public class ClassC < public void methodC()< >public void test() < >>
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
Pro Java
Ключевое слово extends позволяет одному интерфейсу наследовать другой. Синтаксис определения такого наследования аналогичен синтаксису наследования классов, но в отличие от них интерфейсы могут наследоваться от любого множества интерфейсов, а не от одного как классы.
Интерфейс, который расширяет (наследуется) более одного интерфейса, наследует все методы и константы от каждого родителя и может определять собственные дополнительные абстрактные методы и константы.
Когда класс реализует интерфейс, который наследует другие интерфейсы, он должен предоставлять реализации всех методов, определенных внутри цепочки наследования интерфейса.
Приведем простой пример. На котором, я кстати покажу, что интерфейсы и классы могут содержаться в одном java файле, но в таком случае только один класс может быть public, остальные должны быть с пакетным доступом. Эффект ничем не отличается от создания отдельного java-файла для каждого класса и интерфейса. После компиляции получаются отдельные class файлы для каждого интерфейса и класса.
В этом примере интерфейс В наследуется от интерфейса А, в котором определены два метода с реализациями по умолчанию. В интерфейса В определен один метод без реализации по умолчанию.
Как видим в классе Ext1 мы реализовали только метод methB1(), поскольку для него нет реализации по умолчанию. Другие же методы – methA1() и methA2() были унаследованы в своей реализации по умолчанию.
В классе Ext2 мы реализовали все методы интерфейсов А и В переопределив их по своему.
Внимание стоит обратить на строки 54 и 58, где мы создаем объектные интерфейсные ссылки ext2 и ext3 соответственно. Но ссылка ext3 имеет интерфейсный тип А, именно поэтому на ней нельзя вызывать метод methB2(), хотя он у нас и реализован в классе Ext2.
Вывод у программы следующий:
Далее в наследовании интерфейсов начинаются грабельки. А что если в каком либо из родителей и потомков объявлены методы с одинаковой сигнатурой? А что если параметры методов совпадают, но отличается тип возвращаемого значения? И т.д и т.п.
Попробуем со всем этим разобраться… Хотя, возможно, все случаи тут и не перечислю, так как их достаточно много, но основные идеи постараюсь донести. А на остальные случаи грабли вам в помощь 🙂
- Если класс реализует интерфейсы в которых есть методы с одинаковой сигнатурой и возвращаемым значением, но сам не переопределяет реализацию этих одинаковых методов, то тогда возникнет ошибка компиляции. Если же он переопределяет эти методы, то тогда все нормально.
- Если в интерфейсах наследуемых один от другого есть одинаковые методы с реализациями по умолчанию, то предпочтение отдается реализации самого нижнего метода по умолчанию в цепочке, так как действует правило переопределения методов. Но если в классе переопределена реализация этого метода, то предпочтение отдается реализации метода в классе. Это естественное правило. Но благодаря новой форме ключевого слова super, можно обратиться к реализации метода по умолчанию родительского интерфейса из метода интерфейса наследника. Эта форма ключевого слова super выглядит следующим образом:
имя_интерфейса.super.имя_метода();
Если, допустим, из метода с реализацией по умолчанию интрефейса В, который унаследовался от интерфейса А, надо обратиться к методу по умолчанию интерфейса А, то это может выглядеть например так:
Программа генерирует следующий вывод:
Первые четыре строки генерируются имплементацией метода по умолчанию meth() интерфейса D, вызванные на объекте класса Dd. Последняя строка выводится имплементацией метода meth() в классе Cb вызванного на объекте этого класса. Опять же, обратите внимание на строки 47 и 50, на то каким образом определены типы ссылок.