Нужно где-то положить конец этому вопросу. Его раньше любили на собеседованиях. Да и для программиста не лишним будет окончательное понимание как оно работает.
Итак, в Java8 появились дефолтные методы в интерфейсах. Появились как костыль, на котором удержалось backward compatibility. Например, как имплементировать следующий код не положив код написанный в ранних версиях Java?
|
1 2 |
List list = new ArrayList(); list.stream(); |
Ведь если мы добавим метод в интерфейс List, то предыдущие версии попадают с ошибкой, поскольку такого метода нет в их имплементации наследников этого интерфейса. А если я скажу что метод stream() добавлен ещё выше? В интерфейсе Collection. А forEach() в Iterable? Ляжет ВСЁ! Можно было бы создать отдельный утилитный класс типа Collections, который бы предоставлял нужные обёртки. Но выглядело это как некий оверхэд. Поэтому были придуманы дефолтные методы для интерфейсов. Ну и туда же положили статические методы.
В контексте этого поста будет рассмотрен случай когда в родительских интерфейсах существуют методы с одинаковой сигнатурой.
Static
Начнем с простейшего. Со статических методов.
Предлагаю рассмотреть следующий код:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
interface A { static String stat() { return "statA"; } } interface B { static String stat() { return "statB"; } } class C implements A, B { static String stat() { return "statC"; } } public class Static { public static void main(String[] args) { System.out.println("$ " + A.stat()); System.out.println("$ " + B.stat()); System.out.println("$ " + C.stat()); } } //output $ statA $ statB $ statC |
Есть два интерфейса с одним и тем же статическим методом. И класс, который имплементит эти два интерфейса. В данном случае Java не обязывает имплементить статический метод интерфейса. Мы можем добавить такой же статический метод в класс С и он никакого отношения не будет иметь к методам родительских интерфейсов. Собственно, работает так же само как и для обычных классов. Если мы попытаемся добавить аннотацию @Override к методу С.stat() получим ошибку: “The method stat() of type C must override or implement a supertype method”.
То есть статические методы можно использовать смело не боясь конфликтов в имплементациях.
Abstract methods
Рассмотрим следующий отрывок кода:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
interface A { String apply(); } interface B { String apply(); } class C implements A, B { @Override public String apply() { return "methC"; } } public class Abstract { public static void main(String[] args) { System.out.println("$ " + new C().apply()); } } // output $ methC |
Всё как в предыдущем примере. Два одинаковых метода. Класс замечательно переопределяет этот метод. Кажется, ничего сложного нет и кейс проще, чем со статикой. Но давайте изменим возвращаемый тип метода в интерфейсе А. Получим ошибку при компиляции класса С: “The return type is incompatible with A.apply()”. Также интересная ситуация будет с исключением, если его нужно объявить в классе С. В таком случае это же исключение должно быть добавлено ко всем методам родительских интерфейсов. С другой стороны всё проще. Исключения могут быть объявлены в интерфейсах какие угодно и сколько угодно. Переопределяемый метод в классе может их полностью игноировать.
Получается, что абстрактные методы с одинаковой сигнатурой могут быть переопределны в классе только в том случае, если у них одинаковый возвращаемый тип и выбрасываемое исключение “покрывается” (поглощается родительским типом исключения или указано в родительском интерфейсе) всеми родительскими интерфейсами.
Default methods
Ну и наконец дефолтные методы:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
interface A { default String def() { return "defA"; } } interface B { default String def() { return "defB"; } } class C implements A, B { @Override public String def() { // return A.super.def(); // return B.super.def(); return "defC"; } } public class Default { public static void main(String[] args) { System.out.println("$ " + new C().def()); } } // output $ defC |
Как можно видеть переопределение метода в классе С точно такое же как и у обычного абстрактного метода. Внутри метода класса С можно вызывать дефолтный метод из любого родительского интерфейса напрямую, а также можно имплементировать любую вашу логику. Всё остальное также справедливо как и для абстрактных методов.