Что такое default методы в интерфейсе и для чего они были введены
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.
Продолжаем изучение темы «интерфейсы», которую начали на предыдущем занятии и вначале посмотрим как можно прописывать в интерфейсах не только методы, но и константы. Давайте в интерфейсе 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 8
Методы интерфейсов по умолчанию
Лямбда-выражения
Давайте начнем с простого примера: сортировка массива строк в предыдущих версиях языка.
Статический метод Collections.sort принимает список и компаратор, который используется для сортировки списка. Наверняка вам часто приходилось создавать анонимные компараторы для того чтобы передать их в метод.
Java 8 предоставляет гораздо более короткий синтаксис — лямбда-выражения, чтобы вам не приходилось тратить время на создание анонимных объектов:
Как видите, код гораздо короче и куда более читаем. И его можно сделать еще короче:
Компилятору известны типы параметров, поэтому их можно тоже опустить. Давайте посмотрим, как еще могут использовать лямбда-выражения.
Функциональные интерфейсы
Как лямбда-выражения соответствуют системе типов языка Java? Каждой лямбде соответствует тип, представленный интерфейсом. Так называемый функциональный интерфейс должен содержать ровно один абстрактный метод. Каждое лямбда-выражение этого типа будет сопоставлено объявленному методу. Также, поскольку методы по умолчанию не являются абстрактными, вы можете добавлять в функциональный интерфейс сколько угодно таких методов.
Ссылки на методы и конструкторы
Предыдущий пример можно упростить, если использовать статические ссылки на методы:
Давайте посмотрим, как передавать ссылки на конструкторы. Сперва определим бин с несколькими конструкторами:
Затем определим интерфейс фабрики, которая будет использоваться для создания новых персон:
Теперь вместо реализации интерфейса мы соединяем все вместе при помощи ссылки на конструктор:
Области действия лямбд
Однако переменная num должна все равно оставаться неизменяемой. Следующий код не скомпилируется:
Запись в переменную num в пределах лямбда-выражения также запрещена.
Доступ к полям и статическим переменным
В отличии от локальных переменных, мы можем записывать значения в экземплярные поля класса и статические переменные внутри лямбда-выражений. Это поведение хорошо знакомо по анонимным объектам.
Доступ к методам интерфейсов по умолчанию
Внутри лямбда-выражений запрещено обращаться к методам по умолчанию. Следующий код не скомпилируется:
Встроенные функциональные интерфейсы
Однако в Java 8 также появилось много новых функциональных интерфейсов, которые призваны облегчить вам жизнь. Некоторые интерфейсы хорошо известны по библиотеке Google Guava. Даже если вы незнакомы с этой библиотекой, вам стоить взглянуть, как эти интерфейсы были дополнены некоторыми полезными методами расширений.
Предикаты
Функции
Поставщики
Поставщики (suppliers) предоставляют результат заданного типа. В отличии от функций, поставщики не принимают аргументов.
Потребители
Потребители (consumers) представляют собой операции, которые производятся на одним входным аргументом.
Компараторы
Компараторы хорошо известны по предыдущим версиям Java. Java 8 добавляет в интерфейс различные методы по умолчанию.
Опциональные значения
Опциональные значения (optionals) не являются функциональными интерфейсами, однако являются удобным средством предотвращения NullPointerException. Это важная концепция, которая понадобится нам в следующем разделе, поэтому давайте взглянем, как работают опциональные значения.
Опциональные значение — это по сути контейнер для значения, которое может быть равно null. Например, вам нужен метод, который возвращает какое-то значение, но иногда он должен возвращать пустое значение. Вместо того, чтобы возвращать null, в Java 8 вы можете вернуть опциональное значение.
Потоки
Сначала давайте посмотрим, как работать с потоком последовательно. Сперва создадим источник в виде списка строк:
Filter
Операция Filter принимает предикат, который фильтрует все элементы потока. Эта операция является промежуточной, т.е. позволяет нам вызвать другую операцию (например, forEach ) над результатом. ForEach принимает функцию, которая вызывается для каждого элемента в (уже отфильтрованном) поток. ForEach является конечной операцией. Она не возращает никакого значения, поэтому дальнейший вызов потоковых операций невозможен.
Sorted
Операция Sorted является промежуточной операцией, которая возвращает отсортированное представление потока. Элементы сортируются в обычном порядке, если вы не предоставили свой компаратор:
Помните, что sorted создает всего лишь отсортированное представление и не влияет на порядок элементов в исходной коллекции. Порядок строк в stringCollection остается нетронутым:
Match
Для проверки, удовлетворяет ли поток заданному предикату, используются различные операции сопоставления (match). Все операции сопоставления являются конечными и возвращают результат типа boolean.
Count
Reduce
Эта конечная операция производит свертку элементов потока по заданной функции. Результатом является опциональное значение.
Параллельные потоки
Как уже упоминалось выше, потоки могут быть последовательными и параллельными. Операции над последовательными потоками выполняются в одном потоке процессора, над параллельными — используя несколько потоков процессора.
Следующие пример демонстрирует, как можно легко увеличить скорость работы, используя параллельные потоки.
Сперва создадим большой список из уникальных элементов:
Теперь измерим время сортировки этого списка.
Последовательная сортировка
Параллельная сортировка
Ассоциативные массивы
Как уже упоминалось, ассоциативные массивы (maps) не поддерживают потоки. Вместо этого ассоциативные массивы теперь поддерживают различные полезные методы, которые решают часто встречаемые задачи.
Этот код в особых комментариях не нуждается: putIfAbsent позволяет нам не писать дополнительные проверки на null; forEach принимает потребителя, который производит операцию над каждым элементом массива.
Этот код показывает как использовать для вычислений код при помощи различных функций:
Затем мы узнаем, как удалить объект по ключу, только если этот объект ассоциирован с ключом:
Еще один полезный метод:
Объединить записи двух массивов? Легко:
В случае отсутствия ключа Merge создает новую пару ключ-значение. В противном случае — вызывает функцию объединения для существующего значения.
API для работы с датами
Clock
Часовые пояса
LocalTime
Тип LocalTime представляет собой время с учетом часового пояса, например, 10pm или 17:30:15. В следующем примере создаются два местных времени для часовых поясов, определенных выше. Затем оба времени сравниваются, и вычисляется разница между ними в часах и минутах.
Тип LocalTime содержит различные фабричные методы, которые упрощают создание новых экземпляров, а также парсинг строк.
LocalDate
Создание экземпляра LocalDate путем парсинга строки:
LocalDateTime
Форматирование даты-времени работает так же, как и форматирование даты или времени. Мы можем использовать библиотечные или свои собственные шаблоны.
Подробно о синтаксисе шаблонов можно почитать здесь.
Аннотации
Аннотации в Java 8 являются повторяемыми. Давайте сразу посмотрим пример, чтобы понять, что это такое.
Сперва мы определим аннотацию-обертку, которая содержит массив аннотаций:
Вариант 1: использовать аннотацию-контейнер (старый способ)
Вариант 2: использовать повторяемую аннотацию (новый способ)
Более того, аннотации в Java 8 можно использовать еще на двух элементах:
Вот и все
Полный исходный код статьи доступен на GitHub.
Разница между методом по умолчанию в интерфейсе и обычным методом в классе
Метод по умолчанию представляет собой метод, который объявлен в интерфейсе с модификатором default ; его тело всегда представлено блоком. Он предоставляет реализацию по умолчанию для любого класса, который реализует интерфейс без перекрытия метода. Методы по умолчанию отличны от конкретных методов, которые объявляются в классах.
2 ответа 2
default метод не может обращаться к состоянию объекта (полям объекта), так как никакого объекта нет, но может вызвать другие методы и обращаться к статическим данным (константам).
default метод позволяет избежать необходимости изменить все классы, которые реализуют этот интерфейс.
В классе, реализующим интерфейс с default методами, вы можете их переопределить.
Без default этот класс бы не скомпилировался:
Вывод будет следующим:
Одно из различий заключается в том, что все методы (в т.ч. и default ) в интерфейсе неявно объявлены как public и поменять этот модификатор доступа не получится, а в обычном методе в классе Вы можете сами устанавливать различные модификаторы доступа.
Метод по умолчанию представляет собой метод, который объявлен в интерфейсе с модификатором default; его тело всегда представлено блоком. Он предоставляет реализацию по умолчанию для любого класса, который реализует интерфейс без перекрытия метода. Методы по умолчанию отличны от конкретных методов, которые объявляются в классах.
Попробуйте сделать акцент на выделенном тексте, тогда поймете о чем я:
Он предоставляет реализацию по умолчанию для любого класса, который реализует интерфейс без перекрытия метода. Методы по умолчанию отличны от конкретных методов, которые объявляются в классах.
История эволюции интерфейсов в Java
Интерфейс в Java сильно эволюционировал за прошедшие годы. Давайте рассмотрим, какие изменения произошли в процессе его развития.
Оригинальные интерфейсы
Интерфейсы в Java 1.0 были достаточно простыми по сравнению с тем, какие они сейчас. Они могли содержать лишь два типа элементов: константы и публичные абстрактные методы.
Поля-константы
Интерфейсы могут содержать поля, так же как и обычные классы, но с несколькими отличиями:
Даже несмотря на то, что явно это не задано, поле MY_CONSTANT считается публичной статической финальной константой. Вы можете добавить эти модификаторы, но делать это не обязательно.
Абстрактные методы
Наиболее важными элементами интерфейса являются его методы. Методы интерфейса также отличаются от методов обычного класса:
Вложенность
Java 1.1 представила концепцию классов, которые можно размещать внутри других классов. Такие классы бывают двух видов: статические и нестатические. Интерфейсы так же могут содержать внутри себя другие интерфейсы и классы.
Даже если это не задано явно, такие интерфейсы и классы считаются публичными и статическими.
Перечисления и аннотации
В Java 5 были введены ещё два типа: Перечисления и Аннотации. Они также могут быть помещены внутрь интерфейсов.
Обобщенные типы
Java 5 ввела концепцию «дженериков» — обобщенных типов. Вкратце: «дженерики» позволяют вам использовать обобщенный тип вместо указания конкретного типа. Таким образом, вы можете писать код, который работает с различным количеством типов, не жертвуя при этом безопасностью и не предоставляя отдельную реализацию для каждого типа.
В интерфейсах, начиная с Java 5, вы можете определить обобщенный тип, а затем использовать его в качестве типа возвращаемого значения метода или в качестве типа аргумента метода.
Интерфейс Box работает независимо от того, используете ли вы его для хранения объектов типа String, Integer, List, Shoe или каких-либо других.
Статические методы
Начиная с Java 8, вы можете включать в интерфейсы статические методы. Данный подход изменил привычный для нас способ работы интерфейса. Они теперь работают совсем не так, как работали до Java 8. Первоначально все методы в интерфейсах были абстрактными. Это означало, что интерфейс предоставлял лишь сигнатуру, но не реализацию. Реализация оставалась за классами, реализующими ваш интерфейс.
При использовании статических методов в интерфейсах вам нужно также предоставить реализацию тела метода. Чтобы задействовать в интерфейсе такой метод, просто используйте ключевое слово static. Статические методы считаются публичными по умолчанию.
Наследование статических методов
В отличие от обычных статических методов, статические методы в интерфейсах не наследуются. Это означает, что если вы хотите вызвать такой метод, вы должны вызвать его напрямую из интерфейса, а не из реализующего его класса.
Это поведение очень полезно для избежания проблем при множественном наследовании. Представьте, что у вас есть класс, реализующий два интерфейса. У каждого из интерфейсов есть статический метод с одинаковым именем и сигнатурой. Какой из них должен быть использован в первую очередь?
Почему это полезно
Представьте, что у вас есть интерфейс и целый набор вспомогательных методов, которые работают с классами, реализующими этот интерфейс.
Традиционно существовал подход в использовании класса-компаньона. В дополнение к интерфейсу создавался утилитный класс с очень похожим именем, содержащий статические методы, принадлежащие интерфейсу.
Вы можете найти примеры использования данного подхода прямо в JDK: интерфейс java.util.Collection и сопутствующий ему утилитный класс java.util.Collections.
Со статическими методами в интерфейсах этот подход больше не актуален, не нужен и не рекомендован. Теперь вы можете иметь все в одном месте.
Методы по умолчанию
Методы по умолчанию похожи на статические методы тем, что для них вы также должны предоставлять тело. Чтобы объявить метод по умолчанию, просто используйте ключевое слово default.
В отличие от статических методов, методы по умолчанию наследуются классами, реализующими интерфейс. Что важно, такие классы могут при необходимости переопределять их поведение.
Хотя есть одно исключение. В интерфейсе не может быть методов по умолчанию с такой же сигнатурой, как у методов toString, equals и hashCode класса Object. Взгляните на ответ Брайана Гетца, чтобы понять обоснованность такого решения: Allow default methods to override Object’s methods.
Почему это полезно
Идея с реализацией методов прямо в интерфейсе выглядит не совсем правильной. Так почему в первую очередь была введена эта функциональность?
У интерфейсов есть одна проблема. Как только вы предоставите свой API другим людям, он навсегда «окаменеет» (его нельзя будет изменить безболезненно).
По традиции, Java очень серьезно относится к обратной совместимости. Методы по умолчанию предоставляют способ расширить существующие интерфейсы новыми методами. Наиболее важно то, что методы по умолчанию уже предоставляют определенную реализацию. Это означает, что классам, реализующим ваш интерфейс, не нужно реализовывать какие-либо новые методы. Но, если в этом будет необходимость, методы по умолчанию можно будет переопределить в любое время, если их реализация перестанет подходить. Таким образом, вкратце, вы можете предоставить новую функциональность существующим классам, реализующим ваш интерфейс, сохраняя при этом совместимость.
Конфликты
Давайте представим, что у нас есть класс, реализующий два интерфейса. У этих интерфейсов есть метод по умолчанию с одинаковыми именем и сигнатурой.
Теперь один и тот же метод по умолчанию с одной и той же сигнатурой унаследован от двух разных интерфейсов. У каждого интерфейса своя реализация этого метода.
Итак, как наш класс узнает, какую из двух различных реализаций использовать?
Он и не узнает. Приведенный выше код приведет к ошибке компиляции. Если вам требуется заставить его работать, то для этого необходимо переопределить конфликтный метод в вашем классе.
Приватные методы
С появлением Java 8 и введением методов по умолчанию и статических методов, у интерфейсов появилась возможность содержать не только сигнатуры методов, но и их реализации. При написании таких реализаций рекомендуется разделять сложные методы на более простые. Такой код легче переиспользовать, поддерживать и понимать.
Для такой цели вы бы использовали приватные методы, поскольку они могут содержать все детали реализации, которые не должны быть видимы и использованы извне.
К сожалению в Java 8 интерфейс не может содержать приватные методы. Это означает, что вы можете использовать:
Хронологический порядок
Ниже представлен хронологический перечень изменений по версиям Java: