Думаю, что не ошибусь, если скажу, что завершая базовый курс изучения Java каждый слушатель начинает готовиться к будущим собеседованиям. Читает статьи, форумы в поисках советов, а кто-то возможно уже и сходил на свое первое собеседование в этой профессии.
Среди прочих рекомендаций и описаний возможных вопросов на интервью часто встречаются темы нововведений в различных версиях Java 5, 8, 11 и т.д. Да и в лекциях преподаватель периодически обращает внимание, что тот или иной класс или метод появились с такой-то версии. И здесь задаешься вопросом — к чему мне обращать внимание и запоминать что и когда было введено в язык, если учусь я по последней версии, в которую включены все возможные функции.
Частично ответ на этот вопрос озвучивал Валерий в лекции про дату и время. С большой долей вероятности на входе в профессию (на нашей первой работе программистом) мы столкнемся с проектами, написанными на прежних версиях Java. В этом случае понимание возможностей или, что возможно лучше, ограничений функционала будет хорошим подспорьем в первой работе.
Но есть и второй момент. Следует понимать, что в базовом курсе мы учились именно основам Java. Разбирались в принципах ООП, синтаксиса и работе базовых инструментов языка. А часть популярных в обсуждениях нововведений зачастую предназначена для помощи программисту, упрощения и удобства работы. Например лямбда выражения и Stream, изучение которых предполагается в следующем курсе. И такая последовательность изучения верна, т.к. программист должен понимать, что кроется за упрощенным выражением.
На своем первом собеседовании я столкнулся с вопросом “для чего нужны дефолтные методы в интерфейсах?”. И ответ, как не сложно догадаться, можно построить из вопроса. Однако, в лекции об интерфейсах о такой возможности не упоминалось, относится она к нововведениям в Java 8, поэтому предлагаю рассмотреть эту тему подробнее.
Итак, как я уже сказал ранее, суть вытекает из названия темы. Дефолтные методы или методы по умолчанию позволяют включать в интерфейс не только абстрактные методы, но и методы с реализацией. Отличительной особенностью является то, что эти методы не требуют переопределения и они также доступны классам реализующим интерфейс.
Перейдем к примерам. Для наглядности воспользуемся фрагментами кода из лекции “Интерфейсы” (курс: Java Developer, level 1).
Возьмем три класса, описывающих геометрических фигуры, например, “Segment”, “Circle” и “Square”, и создадим интерфейс, который бы позволял сравнивать их по периметру.
public class Segment{ double a; //Длина отрезка Segment(double a) { this.a = a; } } public class Circle{ double radius; Circle(double radius) { this.radius = radius; } } public class Square { double a; //Сторона квадрата public Square(double a){ this.a = a; } }
Далее создаем интерфейс, в который включаем метод для сравнения наших объектов по периметру. В качестве параметра в него можно будет передавать объект, реализующий интерфейс ComparePerimeter. А также для исключения ошибок включим в него метод, обязывающий фигуру определить её периметр.
public interface ComparePerimeter { //Метод для определения периметра double perimeter(); //Метод для сравнения фигур по периметру int comparePerimeters(ComparePerimeter figure); }
После включения интерфейса наши объекты будут выглядеть следующим образом.
public class Segment implements ComparePerimeter{ double a; //Длина отрезка Segment(double a) { this.a = a; } @Override double perimeter() { return a; } @Override public int comparePerimeters(ComparePerimeter figure) { return Double.compare(perimeter(), figure.perimeter()); } } public class Circle implements ComparePerimeter{ double radius; Circle(double radius) { this.radius = radius; } @Override double perimeter() { return 2 * Math.PI * radius; } @Override public int comparePerimeters(ComparePerimeter figure) { return Double.compare(perimeter(), figure.perimeter()); } } public class Square implements ComparePerimeter { double a; public Square(double a){ this.a = a; } @Override public double perimeter() { return 4 * a; } @Override public int comparePerimeters(ComparePerimeter figure) { return Double.compare(perimeter(), figure.perimeter()); } }
Прежде чем перейти к следующему шагу проверим, как работает наш код. Создадим объекты и сравним их периметры.
public static void main(String[] args) { Segment segment = new Segment(5); Circle circle = new Circle(0.5); Square square = new Square(5.0); System.out.println("segment.comparePerimeters(circle) = " + segment.comparePerimeters(circle)); System.out.println("circle.comparePerimeters(square) = " + circle.comparePerimeters(square)); System.out.println("square.comparePerimeters(segment) = " + square.comparePerimeters(segment)); }
Вывод на консоль:
segment.comparePerimeters(circle) = 1 circle.comparePerimeters(square) = -1 square.comparePerimeters(segment) = 1
Но что мы видим? Если содержимое метода perimeter()
для каждого класса уникально, то код метода comparePerimeters(ComparePerimeter figure)
в каждом классе повторяется. Вызвано это необходимостью переопределения метода.
И здесь нам на помощь приходит возможность создания дефолтных методов.
Для того, чтобы метод, включенный в интерфейс, стал дефолтным необходимо использовать ключевое слово default
в определении метода. Перенесем код метода для сравнения фигур в интерфейс.
public interface ComparePerimeter { double perimeter(); default int comparePerimeters(ComparePerimeter figure){ return Double.compare(perimeter(), figure.perimeter()); }; }
После чего, уберем переопределение метода из классов и убедимся, что система не выдаст нам ошибок.
public class Segment implements ComparePerimeter{ double a; //Длина отрезка Segment(double a) { this.a = a; } @Override double perimeter() { return a; } } public class Circle implements ComparePerimeter{ double radius; Circle(double radius) { this.radius = radius; } @Override double perimeter() { return 2 * Math.PI * radius; } } public class Square implements ComparePerimeter { double a; public Square(double a){ this.a = a; } @Override public double perimeter() { return 4 * a; } }
Повторим проверку, чтобы подтвердить то, что метод по-прежнему доступен для объектов.
public static void main(String[] args) { Segment segment = new Segment(5); Circle circle = new Circle(0.5); Square square = new Square(5.0); System.out.println("segment.comparePerimeters(circle) = " + segment.comparePerimeters(circle)); System.out.println("circle.comparePerimeters(square) = " + circle.comparePerimeters(square)); System.out.println("square.comparePerimeters(segment) = " + square.comparePerimeters(segment)); }
Вывод на консоль:
segment.comparePerimeters(circle) = 1 circle.comparePerimeters(square) = -1 square.comparePerimeters(segment) = 1
Итак, мы убедились в том, что в интерфейсы можно включать не только абстрактные методы, но и методы реализующие функции, общие для всех объектов, реализующих интерфейс. Такие методы должны быть определены ключевым словом default
, они не требуют переопределения в классах и доступны всем объектам, реализующим данный интерфейс.
А теперь давайте представим, что нам понадобилось выводить на консоль значения периметров наших фигур. И при этом переопределять toString()
у классов нет ни времени, ни сил.
Для выхода из такого положения снова воспользуемся дефолтными методами. Просто добавим необходимый метод в интерфейс.
public interface ComparePerimeter { double perimeter(); default int comparePerimeters(ComparePerimeter figure){ return Double.compare(perimeter(), figure.perimeter()); }; default void printPerimeter(){ System.out.println("printPerimeter: " + perimeter()); } }
В результате получаем возможность выводить значения на консоль для всех объектов, реализующих интерфейс. Таким образом парой строк мы предоставили новую функцию целой группе объектов.
Проверим.
public static void main(String[] args) { Segment segment = new Segment(5); Circle circle = new Circle(0.5); Square square = new Square(5.0); System.out.println("segment.comparePerimeters(circle) = " + segment.comparePerimeters(circle)); System.out.println("circle.comparePerimeters(square) = " + circle.comparePerimeters(square)); System.out.println("square.comparePerimeters(segment) = " + square.comparePerimeters(segment)); segment.printPerimeter(); circle.printPerimeter(); square.printPerimeter(); }
Вывод на консоль:
segment.comparePerimeters(circle) = 1 circle.comparePerimeters(square) = -1 square.comparePerimeters(segment) = 1 printPerimeter: 5.0 printPerimeter: 3.141592653589793 printPerimeter: 20.0
Методы в интерфейсах, определенные ключевым словом “default”, не требуют переопределения, но и не исключают такой возможности. При необходимости мы всегда можем переопределить дефолтный метод.
Для примера добавим в один из классов единицы измерений.
public class Circle implements ComparePerimeter { double radius; Circle(double radius) { this.radius = radius; } @Override public double perimeter() { return 2 * Math.PI * radius; } @Override public void printPerimeter() { System.out.println("printPerimeter: " + perimeter() + " mm"); } }
Вывод на консоль:
segment.comparePerimeters(circle) = 1 circle.comparePerimeters(square) = -1 square.comparePerimeters(segment) = 1 printPerimeter: 5.0 printPerimeter: 3.141592653589793 mm printPerimeter: 20.0
И напоследок рассмотрим ситуацию, в которой два интерфейса содержат дефолтные методы с одинаковым именем.
Создадим второй интерфейс и включим в него дефолтный метод с аналогичным именем.
public interface PrintPerimeter { double perimeter(); default void printPerimeter(){ System.out.println("Perimeter: " + perimeter() + "мм"); } }
При попытке реализации двух интерфейсов, включающих дефолтные методы с одинаковыми именами, среда разработки выдаст ошибку и предложит переопределить конфликтующий метод.
public class Square implements ComparePerimeter, PrintPerimeter { double a; public Square(double a){ this.a = a; } @Override public void printPerimeter() { } @Override public double perimeter() { return 4 * a; } }
Подведем итоги:
- Дефолтные методы или методы по умолчанию позволяют включать в интерфейс не только абстрактные методы, но и методы с реализацией;
- Для того, чтобы метод, включенный в интерфейс стал дефолтным необходимо использовать ключевое слово “default” в определении метода;
- Дефолтные методы не требуют переопределения, но и не исключают такой возможности;
- При попытке реализации двух интерфейсов, включающих дефолтные методы с одинаковыми именами, среда разработки выдаст ошибку и предложит переопределить конфликтующий метод.
Минкин Михаил, после окончания базового курса, февраль 2021