1. Обзор
В Java 8 появилось несколько совершенно новых функций, включая лямбда-выражения , функциональные интерфейсы , ссылки на методы , потоки , необязательные , а также статические
методы и методы по умолчанию
в интерфейсах.
Мы уже рассмотрели некоторые из этих функций в другой статье . Тем не менее, статические
методы и методы по умолчанию
в интерфейсах сами по себе заслуживают более глубокого изучения.
В этом руководстве мы узнаем , как использовать статические
методы и методы по умолчанию
в интерфейсах, и обсудим некоторые ситуации, в которых они могут быть полезны.
2. Зачем интерфейсам нужны методы по умолчанию
Как и обычные методы интерфейса, методы по умолчанию неявно общедоступны; нет необходимости указывать модификатор public
.
В отличие от обычных методов интерфейса, мы объявляем их с помощью ключевого слова по умолчанию
в начале сигнатуры метода , и они обеспечивают реализацию .
Давайте рассмотрим простой пример:
public interface MyInterface {
// regular interface methods
default void defaultMethod() {
// default method implementation
}
}
Причина, по которой выпуск Java 8 включал методы по умолчанию
, довольно очевиден.
В типичном дизайне, основанном на абстракциях, где интерфейс имеет одну или несколько реализаций, если к интерфейсу добавить один или несколько методов, все реализации также будут вынуждены их реализовать. В противном случае конструкция просто сломается.
Методы интерфейса по умолчанию являются эффективным способом решения этой проблемы. Они позволяют добавлять в интерфейс новые методы, автоматически доступные в реализациях . Поэтому нам не нужно изменять реализующие классы.
Таким образом, обратная совместимость аккуратно сохраняется без рефакторинга разработчиков.
3. Методы интерфейса по умолчанию в действии
Чтобы лучше понять функциональность методов интерфейса по умолчанию
, давайте создадим простой пример.
Предположим, у нас есть наивный интерфейс Vehicle и всего одна реализация.
Их может быть больше, но давайте будем проще:
public interface Vehicle {
String getBrand();
String speedUp();
String slowDown();
default String turnAlarmOn() {
return "Turning the vehicle alarm on.";
}
default String turnAlarmOff() {
return "Turning the vehicle alarm off.";
}
}
Теперь давайте напишем реализующий класс:
public class Car implements Vehicle {
private String brand;
// constructors/getters
@Override
public String getBrand() {
return brand;
}
@Override
public String speedUp() {
return "The car is speeding up.";
}
@Override
public String slowDown() {
return "The car is slowing down.";
}
}
Наконец, давайте определим типичный основной
класс, который создает экземпляр Car
и вызывает его методы:
public static void main(String[] args) {
Vehicle car = new Car("BMW");
System.out.println(car.getBrand());
System.out.println(car.speedUp());
System.out.println(car.slowDown());
System.out.println(car.turnAlarmOn());
System.out.println(car.turnAlarmOff());
}
Обратите внимание, что методы по умолчанию
, turnAlarmOn()
и turnAlarmOff(),
из нашего интерфейса Vehicle
автоматически доступны в классе Car
.
Кроме того, если в какой-то момент мы решим добавить дополнительные методы по умолчанию в интерфейс
Vehicle
, приложение все равно продолжит работу, и нам не придется заставлять класс предоставлять реализации для новых методов.
Наиболее распространенное использование методов интерфейса по умолчанию — постепенное предоставление дополнительных функций для данного типа без разрушения реализующих классов.
Кроме того, мы можем использовать их для обеспечения дополнительной функциональности существующего абстрактного метода :
public interface Vehicle {
// additional interface methods
double getSpeed();
default double getSpeedInKMH(double speed) {
// conversion
}
}
4. Несколько правил наследования интерфейсов
Методы интерфейса по умолчанию — довольно приятная функция, но стоит упомянуть о некоторых предостережениях. Поскольку Java позволяет классам реализовывать несколько интерфейсов, важно знать, что происходит, когда класс реализует несколько интерфейсов, определяющих одни и те же методы по умолчанию
.
Чтобы лучше понять этот сценарий, давайте определим новый интерфейс Alarm
и реорганизуем класс Car :
public interface Alarm {
default String turnAlarmOn() {
return "Turning the alarm on.";
}
default String turnAlarmOff() {
return "Turning the alarm off.";
}
}
С этим новым интерфейсом, определяющим собственный набор методов по умолчанию
, класс Car
будет реализовывать как Vehicle
, так и Alarm
:
public class Car implements Vehicle, Alarm {
// ...
}
В этом случае код просто не скомпилируется, так как возникает конфликт, вызванный множественным наследованием интерфейсов (он же Diamond Problem ). Класс Car
наследует оба набора методов по умолчанию
. Итак, какие из них мы должны назвать?
Чтобы решить эту неоднозначность, мы должны явно предоставить реализацию для методов:
@Override
public String turnAlarmOn() {
// custom implementation
}
@Override
public String turnAlarmOff() {
// custom implementation
}
Мы также можем сделать так, чтобы наш класс использовал методы по умолчанию
одного из интерфейсов .
Давайте посмотрим на пример, в котором используются методы по умолчанию
из интерфейса Vehicle :
@Override
public String turnAlarmOn() {
return Vehicle.super.turnAlarmOn();
}
@Override
public String turnAlarmOff() {
return Vehicle.super.turnAlarmOff();
}
Точно так же мы можем заставить класс использовать методы по умолчанию
, определенные в интерфейсе Alarm :
@Override
public String turnAlarmOn() {
return Alarm.super.turnAlarmOn();
}
@Override
public String turnAlarmOff() {
return Alarm.super.turnAlarmOff();
}
Можно даже заставить класс Car
использовать оба набора методов по умолчанию :
@Override
public String turnAlarmOn() {
return Vehicle.super.turnAlarmOn() + " " + Alarm.super.turnAlarmOn();
}
@Override
public String turnAlarmOff() {
return Vehicle.super.turnAlarmOff() + " " + Alarm.super.turnAlarmOff();
}
5. Методы статического интерфейса
Помимо объявления методов по умолчанию
в интерфейсах, Java 8 также позволяет нам определять и реализовывать статические
методы в интерфейсах .
Поскольку статические
методы не принадлежат конкретному объекту, они не являются частью API классов, реализующих интерфейс; поэтому их нужно вызывать, используя имя интерфейса, предшествующее имени метода .
Чтобы понять, как статические
методы работают в интерфейсах, давайте рефакторим интерфейс Vehicle
и добавим в него статический
служебный метод:
public interface Vehicle {
// regular / default interface methods
static int getHorsePower(int rpm, int torque) {
return (rpm * torque) / 5252;
}
}
Определение статического
метода в интерфейсе идентично определению в классе. Более того, статический
метод можно вызывать внутри других статических
методов и методов по умолчанию
.
Предположим, что мы хотим рассчитать мощность двигателя данного автомобиля. Мы просто вызываем метод getHorsePower()
:
Vehicle.getHorsePower(2500, 480));
Идея статических
методов интерфейса состоит в том, чтобы предоставить простой механизм, который позволяет нам повысить степень связности дизайна, объединяя связанные методы в одном месте без необходимости создавать объект.
То же самое можно сделать с абстрактными классами. Основное отличие состоит в том, что абстрактные классы могут иметь конструкторы, состояние и поведение .
Кроме того, статические методы в интерфейсах позволяют группировать связанные служебные методы без необходимости создавать искусственные служебные классы, которые являются просто заполнителями для статических методов.
6. Заключение
В этой статье мы подробно рассмотрели использование статических
методов интерфейса и методов интерфейса по умолчанию
в Java 8. На первый взгляд, эта функция может показаться немного небрежной, особенно с точки зрения объектно-ориентированного пуриста. В идеале интерфейсы не должны инкапсулировать поведение, и мы должны использовать их только для определения общедоступного API определенного типа.
Однако когда дело доходит до обеспечения обратной совместимости с существующим кодом, статические
методы и методы по умолчанию
являются хорошим компромиссом.
Как обычно, все примеры кода, показанные в этой статье, доступны на GitHub .