что такое дефолтные методы интерфейсов для чего они нужны
Defaut method в Java
Default-методы появились Java 8. В это статье рассказывается, что это такое, зачем появилось, и как ими пользоваться.
Default-метод — это метод, который реализуется прямо в интерфейсе, его помечают ключевым словом default.
Пример использования
Допустим, у нас есть интерфейс Animal:
Есть классы Cat и Fish, реализующие интерфейс Animal:
Мы хотим добавить в интерфейс Animal метод sleep(), при этом не реализовывать его в каждом классе, а реализовать непосредственно в интерфейсе. Классы же будут наследовать этот метод по умолчанию. Для этого наш метод надо обозначить как default:
Теперь этот метод унаследуют все животные:
Впрочем, его можно и переопределить в каком-либо из классов, например в Fish:
Убедимся, что рыба спит по-своему:
Как наследуются default-методы
Возникает вопрос, какой метод унаследует класс, реализующий два интерфейса, если оба из них содержат default-методы с одинаковыми именами.
Например, есть второй интерфейс Man, который тоже содержит свой default метод sleep():
И есть класс Kentavr, реализующий как интерфейс Man, так и Animal. Какой же метод sleep() унаследует Kentavr?
Чтобы не было неопределенности (и чтобы скомпилировался код), мы обязаны переопределить в Kentavr метод sleep(), причем можно просто вызвать в нем метод sleep() любого из интерфейсов — Man либо Animal, указав через точку и super, чей именно метод нужен:
Убедимся, что кентавр спит по-человечески:
Причины появления default-методов
Наверно уже понятно, что default-методы упрощают рефакторинг — а именно, добавление новых методов.
До Java 8 все методы в интерфейсах были абстрактными. К чему это вело?
К тому, что при добавлении нового метода в интерфейс приходилось править все классы, реализующие интерфейс — реализовывать метод в этих классах. Это было неудобно. А в Java 8 (в классы ядра) захотели ввести новые методы в старые интерфейсы. Так что ввели ключевое слово default и эти методы сделали default. Например, в интерфейсе java.lang.Iterable появились новые default-методы forEach() и spliterator():
Мы рассмотрели, что такое default метод в Java. Код примеров доступен на GitHub.
Для чего пригодится дефолтная реализация интерфейсов?
Расширение интерфейсов с сохранением обратной совместимости
Самый распространенный сценарий — это безопасное добавление методов в интерфейс, уже опубликованный и использующийся бесчисленным множеством клиентов
Решаемая проблема заключается в том, что каждый класс, унаследованный от интерфейса, обязан предоставить реализацию для нового метода. Это не очень затруднительно, когда интерфейс используется только вашим собственным кодом, но если он находится в публичной библиотеке или используется другими командами, то добавление нового элемента интерфейса может вылиться в большую головную боль.
Если я хочу добавить новый GetTopSpeed() метод в этот интерфейс, мне нужно добавить его имплементацию в Avalon:
Однако, если я создам дефолтную реализацию метода GetTopSpeed() в ICar, то у меня не будет необходимости добавлять его в каждый наследующийся класс.
При необходимости, я все ещё могу перегрузить реализацию в классах, для которых не подходит дефолтная:
Важно учитывать, что дефолтный метод GetTopSpeed() будет доступен только для переменных, приведенных к ICar и не будет доступен для Avalon, если в нём нет перегрузки. Это означает, что эта техника наиболее полезна в случае, если вы работаете именно с интерфейсами (иначе ваш код заполонит множество приведений к интерфейсам для получения доступа к дефолтной имплементации метода).
Миксины и трейты (или типа того)
Похожие языковые концепции миксинов и трейтов описывают способы расширения поведения объекта путем композиции без необходимости множественного наследования.
Википедия сообщает о миксинах следующее:
Миксин так же может рассматриваться как интерфейс с реализованными по умолчанию методами
Но, всё-таки, даже с дефолтной реализацией, интерфейсы в C# не являются миксинами. Отличие в том, что они так же могут содержать и методы без имплементации, поддерживают наследование от других интерфейсов, могут быть специализированы (видимо, имеются в виду ограничения шаблонов. — прим. перев.) и так далее. Однако, если мы сделаем интерфейс, который содержит только методы с реализацией по умолчанию, — это будет, по сути, традиционный миксин.
Рассмотрим следующий код, который добавляет объекту функционал «движения» и отслеживания его местоположения (например, в геймдеве):
Ой! В этом коде есть проблема, которую я не замечал до тех пор, пока не начал писать этот пост и не попытался скомпилировать пример. Интерфейсы (даже те, которые имеют дефолтную реализацию) не могут хранить состояние. Следовательно, интерфейсы не поддерживают автоматические свойства. Из документации:
Интерфейсы не могут хранить состояние экземпляра. Не смотря на то, что статические поля в интерфейсах теперь разрешены, экземплярные поля использовать по-прежнему нельзя. Следовательно, нельзя использовать и автоматические свойства, так как они неявно используют скрытые поля.
В этом C# интерфейсы и расходятся с концепцией миксинов (насколько я их понимаю, миксины концептуально могут хранить состояние), но мы все ещё можем достичь изначальной цели:
Таким образом мы достигли желаемого, сделав метод Move() и его реализацию доступной всем классам, которые реализуют интерфейс IMovable. Конечно, классу все ещё нужно предоставить реализацию для свойств, но, по крайней мере, они объявлены в IMovable интерфейсе, что позволяет дефолтной реализации Move() с ними работать и гарантирует, что любой класс, реализующий интерфейс, будет иметь корректное состояние.
Как более полный и практический пример, рассмотрим миксин для логгирования:
Теперь в любом классе я могу унаследоваться от ILogger интерфейса:
Замена методов-расширений
Самое полезное применение, которое я нашел, это замена большого количества методов-расширений. Давайте вернемся к простому примеру логгирования:
До появления дефолтной имплементации в интерфейсах, я бы, как правило, написал множество методов-расширений к этому интерфейсу, чтобы в унаследованном классе нужно было реализовать только один метод, в результате чего пользователи получили бы доступ к множеству расширений:
Этот подход отлично работает, но не лишен недостатков. Например, пространства имен класса с расширениями и интерфейса не обязательно совпадают. Плюс раздражает визуальный шум в виде параметра и ссылки на экземпляр логгера:
Теперь я могу заменить расширения дефолтными реализациями:
Я нахожу такую имплементацию более чистой и удобной для чтения (и поддержки).
Использование реализации по умолчанию также имеет ещё несколько преимуществ перед методами-расширениями:
Чтобы решить эту проблему, я начал объявлять интерфейсы, имеющие члены с реализацией по умолчанию, как partial (кроме разве что совсем простых). Затем я кладу дефолтные реализации в отдельный файл с конвенцией именования вида «ILogger.LogInfoDefaults.cs», «ILogger.LogErrorDefaults.cs» и так далее. Если дефолтных реализаций немного и нет необходимости в дополнительной группировке, то я именую файл «ILogger.Defaults.cs».
Это разделяет члены с дефолтной реализацией от неимплементированного контракта, который обязаны реализовывать унаследовавшиеся классы. Кроме того, это позволяет сократить очень длинные файлы. Ещё существует хитрый трюк с визуализацией вложенных файлов в стиле ASP.NET в проектах любого формата. Для этого добавьте в файл проекта или в Directory.Build.props:
Теперь вы можете выбрать «File Nesting» в Solution Explorer и все ваши .Defaults.cs файлы отобразятся как потомки «основного» файла интерфейса.
В заключение, все ещё есть несколько ситуаций, в которых предпочтительны методы-расширения:
Для чего пригодится дефолтная реализация интерфейсов?
Расширение интерфейсов с сохранением обратной совместимости
Самый распространенный сценарий — это безопасное добавление методов в интерфейс, уже опубликованный и использующийся бесчисленным множеством клиентов
Решаемая проблема заключается в том, что каждый класс, унаследованный от интерфейса, обязан предоставить реализацию для нового метода. Это не очень затруднительно, когда интерфейс используется только вашим собственным кодом, но если он находится в публичной библиотеке или используется другими командами, то добавление нового элемента интерфейса может вылиться в большую головную боль.
Если я хочу добавить новый GetTopSpeed() метод в этот интерфейс, мне нужно добавить его имплементацию в Avalon:
Однако, если я создам дефолтную реализацию метода GetTopSpeed() в ICar, то у меня не будет необходимости добавлять его в каждый наследующийся класс.
При необходимости, я все ещё могу перегрузить реализацию в классах, для которых не подходит дефолтная:
Важно учитывать, что дефолтный метод GetTopSpeed() будет доступен только для переменных, приведенных к ICar и не будет доступен для Avalon, если в нём нет перегрузки. Это означает, что эта техника наиболее полезна в случае, если вы работаете именно с интерфейсами (иначе ваш код заполонит множество приведений к интерфейсам для получения доступа к дефолтной имплементации метода).
Миксины и трейты (или типа того)
Похожие языковые концепции миксинов и трейтов описывают способы расширения поведения объекта путем композиции без необходимости множественного наследования.
Википедия сообщает о миксинах следующее:
Миксин так же может рассматриваться как интерфейс с реализованными по умолчанию методами
Но, всё-таки, даже с дефолтной реализацией, интерфейсы в C# не являются миксинами. Отличие в том, что они так же могут содержать и методы без имплементации, поддерживают наследование от других интерфейсов, могут быть специализированы (видимо, имеются в виду ограничения шаблонов. — прим. перев.) и так далее. Однако, если мы сделаем интерфейс, который содержит только методы с реализацией по умолчанию, — это будет, по сути, традиционный миксин.
Рассмотрим следующий код, который добавляет объекту функционал «движения» и отслеживания его местоположения (например, в геймдеве):
Ой! В этом коде есть проблема, которую я не замечал до тех пор, пока не начал писать этот пост и не попытался скомпилировать пример. Интерфейсы (даже те, которые имеют дефолтную реализацию) не могут хранить состояние. Следовательно, интерфейсы не поддерживают автоматические свойства. Из документации:
Интерфейсы не могут хранить состояние экземпляра. Не смотря на то, что статические поля в интерфейсах теперь разрешены, экземплярные поля использовать по-прежнему нельзя. Следовательно, нельзя использовать и автоматические свойства, так как они неявно используют скрытые поля.
В этом C# интерфейсы и расходятся с концепцией миксинов (насколько я их понимаю, миксины концептуально могут хранить состояние), но мы все ещё можем достичь изначальной цели:
Таким образом мы достигли желаемого, сделав метод Move() и его реализацию доступной всем классам, которые реализуют интерфейс IMovable. Конечно, классу все ещё нужно предоставить реализацию для свойств, но, по крайней мере, они объявлены в IMovable интерфейсе, что позволяет дефолтной реализации Move() с ними работать и гарантирует, что любой класс, реализующий интерфейс, будет иметь корректное состояние.
Как более полный и практический пример, рассмотрим миксин для логгирования:
Теперь в любом классе я могу унаследоваться от ILogger интерфейса:
Замена методов-расширений
Самое полезное применение, которое я нашел, это замена большого количества методов-расширений. Давайте вернемся к простому примеру логгирования:
До появления дефолтной имплементации в интерфейсах, я бы, как правило, написал множество методов-расширений к этому интерфейсу, чтобы в унаследованном классе нужно было реализовать только один метод, в результате чего пользователи получили бы доступ к множеству расширений:
Этот подход отлично работает, но не лишен недостатков. Например, пространства имен класса с расширениями и интерфейса не обязательно совпадают. Плюс раздражает визуальный шум в виде параметра и ссылки на экземпляр логгера:
Теперь я могу заменить расширения дефолтными реализациями:
Я нахожу такую имплементацию более чистой и удобной для чтения (и поддержки).
Использование реализации по умолчанию также имеет ещё несколько преимуществ перед методами-расширениями:
Чтобы решить эту проблему, я начал объявлять интерфейсы, имеющие члены с реализацией по умолчанию, как partial (кроме разве что совсем простых). Затем я кладу дефолтные реализации в отдельный файл с конвенцией именования вида «ILogger.LogInfoDefaults.cs», «ILogger.LogErrorDefaults.cs» и так далее. Если дефолтных реализаций немного и нет необходимости в дополнительной группировке, то я именую файл «ILogger.Defaults.cs».
Это разделяет члены с дефолтной реализацией от неимплементированного контракта, который обязаны реализовывать унаследовавшиеся классы. Кроме того, это позволяет сократить очень длинные файлы. Ещё существует хитрый трюк с визуализацией вложенных файлов в стиле ASP.NET в проектах любого формата. Для этого добавьте в файл проекта или в Directory.Build.props:
Теперь вы можете выбрать «File Nesting» в Solution Explorer и все ваши .Defaults.cs файлы отобразятся как потомки «основного» файла интерфейса.
В заключение, все ещё есть несколько ситуаций, в которых предпочтительны методы-расширения:
Продолжаем изучение темы «интерфейсы», которую начали на предыдущем занятии и вначале посмотрим как можно прописывать в интерфейсах не только методы, но и константы. Давайте в интерфейсе GeomInterface (из предыдущего занятия) пропишем две вот такие переменные:
Почему эти переменные я называю константами? Дело в том, что в Java к этим определениям автоматически добавляются ключевые слова:
public static final
и любые переменные превращаются в общедоступные статические константы. То есть, в интерфейсах попросту нельзя объявлять переменные – только константы. Далее, мы можем использовать MIN_COORD и MAX_COORD в классах, где применен интерфейс GeomInterface. Например, в классе Line:
Смотрите, мы здесь объявили сеттер setCoord и в нем проверяем соответствие переданных координат диапазону [MIN_COORD; MAX_COORD] с помощью вспомогательного приватного метода isCheck. Наличие констант как раз и объясняется их объявлением в интерфейсе GeomInterface.
Статические методы в интерфейсах
Но если в интерфейсе можно объявлять статические константы, то можно ли задавать и статические методы? Да, это стало возможно, начиная с версии JDK 8, и делается очевидным образом:
Здесь объявлен статический метод showInterval, который должен иметь реализацию. То есть, объявлять такие методы без реализации уже нельзя. И они не могут быть переопределены в классах.
Мы уже говорили с вами, что такое статические переменные и методы и как они себя ведут (https://www.youtube.com/watch?v=jEUXJRsHwmY). Я не стану здесь повторяться. Отмечу лишь, что это метод, располагающийся в строго определенной области памяти на всем протяжении работы программы. Следовательно, к нему можно обратиться и вызвать непосредственно из интерфейса, в котором он определен. Например, так:
Точно также к нему следует обращаться и из экземпляров классов, например:
Фактически, мы получаем неизменяемые методы, объявленные внутри интерфейса.
Вложенные интерфейсы и их расширение
Далее, интерфейсы можно объявлять внутри классов. Делается это очевидным образом, и я здесь приведу лишь отвлеченный пример. Пусть имеется класс InterfaceGroup, в котором определены два интерфейса: Interface_1 и Interface_2:
То есть, класс как бы образует группу интерфейсов. Далее, для их применения в классах, используется следующий синтаксис:
Мы здесь указываем сначала имя класса, а затем, через точку имя интерфейса. Но так можно делать, если модификатор доступа позволяет обратиться к интерфейсу извне. Например, если у первого указать модификатор private:
то возникнет ошибка. Такой интерфейс можно использовать только внутри класса. Как? Например, для расширения других публичных интерфейсов. Расширение – это когда один интерфейс наследуется от другого. В частности, мы можем расширить Interface_2, следующим образом:
То есть, здесь используется тот же синтаксис, что и при наследовании классов, только применительно к интерфейсам. В результате такой операции, второй интерфейс унаследует все публичные элементы первого. И, затем, указывая его в классе ReleaseInterface, должны определить уже два метода:
Приватные методы интерфейса
Но что значит: наследуются все публичные методы интерфейса? Разве в интерфейсах методы и константы могут быть не публичными? Да, начиная с версии JDK 9, допускается в интерфейсах объявлять приватные методы. Конечно, они обязательно должны иметь реализацию и используются исключительно внутри интерфейса. Например, мы можем объявить такой приватный метод:
Тогда при расширении второго интерфейса этот метод унаследован не будет.
Приватные методы используются исключительно для внутреннего использования, например, для программирования повторяющихся действий, когда мы используем описание метода с реализацией по умолчанию. Об этом речь пойдет дальше.
Интерфейсы с абстрактными классами
Давайте теперь зададимся вопросом: а можно ли к абстрактному классу применять интерфейсы? Оказывается, да, можно и при этом реализация интерфейсного метода getSquare в нем может отсутствовать:
В этом случае метод getSquare обязательно должен быть определен в дочернем классе. Если же этот метод прописать непосредственно в классе Geom:
То дочерние классы могут его не переопределять. Тогда при обращении к getSquare() будет возвращаться значение 0. Благодаря такой гибкости, мы можем в программе реализовывать самую разную логику взаимодействия с интерфейсами.
Методы с реализацией по умолчанию
Фактически вот этот последний пример позволяет использовать абстрактный класс для определения метода getSquare с реализацией по умолчанию (то есть, его действие, когда он не переопределяется в дочерних классах). Так приходилось делать до версии JDK 8, чтобы не «заставлять» программистов определять методы интерфейса, если это не требовалось. Теперь (начиная с JDK 8 и выше) в интерфейсах можно определять методы с реализацией по умолчанию и такие методы можно не переопределять в классах. Для их объявления используется следующий синтаксис:
default ([аргументы]) <
[операторы]
>
Например, определим в интерфейсе MathGeom метод getSquare с реализацией по умолчанию:
И применим его ко всем классам графических примитивов:
Смотрите, в классе Line мы не переопределяли метод getSquare, а в классах Rectangle и Triangle он переопределен. Теперь, создавая экземпляры этих классов в функции main:
мы можем совершенно свободно вызывать у них метод getSquare:
Обратите внимание, нам здесь сначала нужно привести ссылку g[i] к типу MathGeom и только потом вызвать метод getSquare. В консоли увидим значения:
Здесь первый ноль был получен из реализации метода по умолчанию для класса Line. Остальные значения – из переопределенных методов. То есть, теперь, мы можем не прописывать реализацию метода getSquare в классах примитивов, если она нам не нужна. И это добавляет дополнительное удобство при программировании.
Но что будет, если в GeomInterface также определить метод getSquare с реализацией по умолчанию:
Тогда для класса Line, который применяет оба интерфейса, какая реализация будет использована? В действительности, никакая. Виртуальная машина Java в этом случае выдаст ошибку и потребуется явное определение этого метода. И это можно сделать так:
то он будет вызван из интерфейса MathGeom и вернет значение 0. Вот так, непосредственно из экземпляра класса можно обращаться к объектам интерфейсов и использовать их элементы. Конечно, это имеет смысл только в условиях неопределенности, например, как в нашем случае. Иначе, достаточно просто записать имя метода или константы и она будет взята из соответствующего интерфейса или базового класса.
Заключение
Если вы четко представляете все эти моменты, то вы в целом знаете что такое интерфейсы в Java и как ими пользоваться. Конечно, правильное использование любой конструкции языка приходит с опытом и интерфейсы здесь далеко не исключение. Поэтому предлагаю пройти определенный путь кодера и совершить следующий великий подвиг.
Великий подвиг. Объявить класс DataGraph для хранения данных для графика в виде массива вещественных чисел размерностью N элементов (число N задать как константу, например, N=10). Записать отдельные классы (НЕ дочерние): LineGraph (точки в графике соединяются линиями), BarGraph (график в виде столбцов), ChartGraph (график в виде круговой диаграммы). При создании экземпляров этих классов они должны хранить ссылку на объект класса DataGraph. При рисовании графиков, данные следует брать через публичный метод getData() (класса DataGraph), т.е. получать ссылку на массив из N вещественных чисел. Взаимодействие между объектами классов должно выглядеть так:
Далее, объявить интерфейс Observer с методом update() и применить его к классам LineGraph, BarGraph и ChartGraph. По методу update() должно происходить обновление данных и перерисовка графика. В классе DataGraph хранить массив graphs для экземпляров классов LineGraph, BarGraph и ChartGraph. Как только происходит изменение данных в массиве data, вызывать метод update через ссылки graphs. (Изменение данных делать искусственно, например, в программе поменять данные, а затем, вызвать некий метод в DataGraph для запуска вызовов update).
Если все сделать правильно, то управление перерисовкой графиков будет выполняться через интерфейс Observer и благодаря этому классы графиков могут иметь любую структуру наследования, т.к. мы у них не задаем никаких базовых классов. В этом преимущество данной схемы реализации.
Для самых неистовых. В данной реализации класс DataGraph должен иметь только один экземпляр. Поэтому здесь целесообразно реализовать метод getInstance(), который бы возвращал ссылку на объект класса и контролировал бы единственность этого объекта. При этом нужно закрыть возможность создавать экземпляр класса напрямую через оператор new.
Видео по теме
#11 Концепция объектно-ориентированного программирования (ООП)
Дефолтные методы интерфейсов
Думаю, что не ошибусь, если скажу, что завершая базовый курс изучения Java каждый слушатель начинает готовиться к будущим собеседованиям. Читает статьи, форумы в поисках советов, а кто-то возможно уже и сходил на свое первое собеседование в этой профессии.
Среди прочих рекомендаций и описаний возможных вопросов на интервью часто встречаются темы нововведений в различных версиях Java 5, 8, 11 и т.д. Да и в лекциях преподаватель периодически обращает внимание, что тот или иной класс или метод появились с такой-то версии. И здесь задаешься вопросом — к чему мне обращать внимание и запоминать что и когда было введено в язык, если учусь я по последней версии, в которую включены все возможные функции.
Частично ответ на этот вопрос озвучивал Валерий в лекции про дату и время. С большой долей вероятности на входе в профессию (на нашей первой работе программистом) мы столкнемся с проектами, написанными на прежних версиях Java. В этом случае понимание возможностей или, что возможно лучше, ограничений функционала будет хорошим подспорьем в первой работе.
Но есть и второй момент. Следует понимать, что в базовом курсе мы учились именно основам Java. Разбирались в принципах ООП, синтаксиса и работе базовых инструментов языка. А часть популярных в обсуждениях нововведений зачастую предназначена для помощи программисту, упрощения и удобства работы. Например лямбда выражения и Stream, изучение которых предполагается в следующем курсе. И такая последовательность изучения верна, т.к. программист должен понимать, что кроется за упрощенным выражением.
На своем первом собеседовании я столкнулся с вопросом “для чего нужны дефолтные методы в интерфейсах?”. И ответ, как не сложно догадаться, можно построить из вопроса. Однако, в лекции об интерфейсах о такой возможности не упоминалось, относится она к нововведениям в Java 8, поэтому предлагаю рассмотреть эту тему подробнее.
Итак, как я уже сказал ранее, суть вытекает из названия темы. Дефолтные методы или методы по умолчанию позволяют включать в интерфейс не только абстрактные методы, но и методы с реализацией. Отличительной особенностью является то, что эти методы не требуют переопределения и они также доступны классам реализующим интерфейс.
Перейдем к примерам. Для наглядности воспользуемся фрагментами кода из лекции “Интерфейсы” (курс: Java Developer, level 1).
Возьмем три класса, описывающих геометрических фигуры, например, “Segment”, “Circle” и “Square”, и создадим интерфейс, который бы позволял сравнивать их по периметру.
Далее создаем интерфейс, в который включаем метод для сравнения наших объектов по периметру. В качестве параметра в него можно будет передавать объект, реализующий интерфейс ComparePerimeter. А также для исключения ошибок включим в него метод, обязывающий фигуру определить её периметр.
После включения интерфейса наши объекты будут выглядеть следующим образом.
Прежде чем перейти к следующему шагу проверим, как работает наш код. Создадим объекты и сравним их периметры.
Но что мы видим? Если содержимое метода perimeter() для каждого класса уникально, то код метода comparePerimeters(ComparePerimeter figure) в каждом классе повторяется. Вызвано это необходимостью переопределения метода.
И здесь нам на помощь приходит возможность создания дефолтных методов.
Для того, чтобы метод, включенный в интерфейс, стал дефолтным необходимо использовать ключевое слово default в определении метода. Перенесем код метода для сравнения фигур в интерфейс.
После чего, уберем переопределение метода из классов и убедимся, что система не выдаст нам ошибок.
Повторим проверку, чтобы подтвердить то, что метод по-прежнему доступен для объектов.
А теперь давайте представим, что нам понадобилось выводить на консоль значения периметров наших фигур. И при этом переопределять toString() у классов нет ни времени, ни сил.
Для выхода из такого положения снова воспользуемся дефолтными методами. Просто добавим необходимый метод в интерфейс.
В результате получаем возможность выводить значения на консоль для всех объектов, реализующих интерфейс. Таким образом парой строк мы предоставили новую функцию целой группе объектов.
Методы в интерфейсах, определенные ключевым словом “default”, не требуют переопределения, но и не исключают такой возможности. При необходимости мы всегда можем переопределить дефолтный метод.
Для примера добавим в один из классов единицы измерений.
И напоследок рассмотрим ситуацию, в которой два интерфейса содержат дефолтные методы с одинаковым именем.
Создадим второй интерфейс и включим в него дефолтный метод с аналогичным именем.
При попытке реализации двух интерфейсов, включающих дефолтные методы с одинаковыми именами, среда разработки выдаст ошибку и предложит переопределить конфликтующий метод.
Минкин Михаил, после окончания базового курса, февраль 2021