Перейти к основному содержимому

Статические методы и методы по умолчанию в интерфейсах Java

· 6 мин. чтения

Задача: Наибольшая подстрока палиндром

Для заданной строки s, верните наибольшую подстроку палиндром входящую в s. Подстрока — это непрерывная непустая последовательность символов внутри строки. Стока является палиндромом, если она читается одинаково в обоих направлениях...

ANDROMEDA 42

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 .