Методы default и static в интерфейсах Java
Добавляйте методы default и static в интерфейсы Java для развития API без нарушения существующих реализаций.
До Java 7 интерфейс мог объявлять только абстрактные методы — тела методов находились в реализующих классах. Java 8 добавила три новых элемента, которые можно помещать в интерфейс:
- Методы
default— метод с телом, который наследуется реализациями, не переопределяющими его. - Методы
static— вспомогательные методы, принадлежащие самому интерфейсу. - Методы
private(Java 9) — вспомогательные методы, общие для методовdefaultиstaticинтерфейса.
Основным мотивом стала эволюция API. Когда java.util.Collection существовал уже пятнадцать лет и имел миллионы реализаций, команда Java захотела добавить stream(), не нарушая ни одну из них. Именно это и сделал default Stream<E> stream() { ... }.
Методы default
Метод default имеет тело. Реализующие классы наследуют его бесплатно и могут переопределить, если нужно другое поведение:
public interface Greeter {
String name();
default String greet() {
return "Hello, " + name() + "!";
}
}
public class English implements Greeter {
public String name() { return "Alice"; }
}
public class Loud implements Greeter {
public String name() { return "Bob"; }
public String greet() { return "HEY " + name().toUpperCase() + "!"; } // override
}
new English().greet(); // Hello, Alice!
new Loud().greet(); // HEY BOB!default — это просто маркер: ключевое слово сообщает компилятору, что это тело метода внутри интерфейса. Без него метод интерфейса не имеет тела.
Как и любой не-private член интерфейса, метод default неявно является public; его нельзя сделать protected или пакетно-приватным. В примерах выше нет ключевого слова public, потому что это единственный вариант, который допускает компилятор.
Добавление метода default к существующему интерфейсу — это неломающее изменение. Реализации, не переопределяющие его, наследуют значение по умолчанию. Именно это свойство делает возможной эволюцию интерфейсов.
Статические методы в интерфейсах
Метод static в интерфейсе принадлежит интерфейсу, а не экземплярам. Вызывайте его через имя интерфейса:
public interface Path {
static Path of(String s) { return new SimplePath(s); }
String value();
}
Path p = Path.of("/tmp/foo");Распространённое применение — фабричные методы, создающие экземпляры интерфейса: Path.of, List.of, Map.of, Stream.of. Они позволяют вызывающему коду зависеть от интерфейса даже при создании объектов, не указывая конкретный класс реализации.
Статические методы интерфейса не наследуются. Их всегда вызывают через имя интерфейса, но не через подкласс.
Приватные методы (Java 9+)
Метод private в интерфейсе — это вспомогательный метод, видимый только другим методам того же интерфейса. Он позволяет двум методам default совместно использовать логику, не раскрывая её реализациям:
public interface Logger {
default void info(String msg) { log("INFO", msg); }
default void warn(String msg) { log("WARN", msg); }
private void log(String level, String msg) {
System.out.println("[" + level + "] " + msg);
}
}Без private пришлось бы либо копировать тело в оба метода default, либо объявлять log как метод default — что позволило бы реализациям его переопределять, а это, скорее всего, нежелательно.
Что методы default не могут делать
Методы default существуют в интерфейсе, у которого нет состояния. Они могут вызывать абстрактные методы и другие методы default/static, но не могут читать или записывать поля экземпляра — их попросту нет.
public interface Counter {
int get();
void increment();
default void incrementTwice() { // ok — only calls abstract methods
increment();
increment();
}
}Это ограничивает объём реального поведения метода default. Они лучше всего подходят для тонких удобных слоёв поверх абстрактных методов, а не для полной замены реализации.
Конфликты ромбовидного наследования
Когда класс реализует два интерфейса, оба из которых предоставляют метод default с одинаковой сигнатурой, класс должен явно разрешить конфликт:
interface A { default String hello() { return "A"; } }
interface B { default String hello() { return "B"; } }
class C implements A, B {
@Override
public String hello() {
return A.super.hello() + B.super.hello(); // explicit pick
}
}A.super.hello() вызывает метод A по умолчанию; B.super.hello() — метод B. Компилятор откажется компилировать class C implements A, B { } без переопределения — автоматического победителя нет. Это ответ Java на «проблему ромба» множественного наследования: когда она возникает, класс должен сделать выбор.
Конкретный суперкласс также имеет приоритет над унаследованным методом по умолчанию: если суперкласс предоставляет hello(), он выигрывает у любого метода default из интерфейса.
Когда добавлять метод default
default — правильный инструмент, когда:
- Вы хотите расширить существующий, широко реализованный интерфейс, не нарушая реализации.
- Новый метод действительно выводится из существующих абстрактных методов.
- Значение по умолчанию «очевидно правильно» — большинство реализаций будут им довольны.
Это неправильный инструмент, когда:
- Возникает соблазн разместить там реальное поведение, основанное на состоянии (вместо этого используйте абстрактный класс).
- Значение по умолчанию фактически бесполезно, и все реализации всё равно его переопределят (просто оставьте метод абстрактным).
Рабочий пример
Что дальше
Интерфейсы и абстрактные классы описывают отношения между типами в разных файлах. Следующая подтема — вложенные классы — рассказывает об отношениях типов внутри одного файла: классах, объявленных внутри других классов, и четырёх вариантах, которые предлагает Java. Перейдите к вложенным классам.