Интерфейсы Java
Определяйте контракты в Java с помощью интерфейсов — абстрактные методы, методы по умолчанию и множественное наследование типов.
Интерфейс — это контракт: именованный набор операций, которые любой реализующий класс обязуется предоставить. У интерфейсов нет состояния экземпляра, нет конструкторов и (за одним исключением, рассматриваемым в следующей главе) нет тел методов. Они описывают что может делать тип, оставляя каждое как на усмотрение реализаций.
Интерфейсы — ответ Java на множественное наследование. Класс расширяет ровно один класс, но может реализовывать любое количество интерфейсов — позволяя смешивать Comparable, AutoCloseable и Iterable на одном типе без неоднозначности.
Объявление интерфейса
Используйте interface вместо class:
public interface Shape {
double area(); // implicitly public abstract
double perimeter();
}Объявления методов в интерфейсе неявно имеют модификаторы public abstract. Их можно писать явно, но большинство руководств по стилю опускают их как излишние — они избыточны.
Реализация интерфейса
Класс объявляет реализацию с помощью implements. Класс должен предоставить тело для каждого метода, объявленного в интерфейсе:
public class Circle implements Shape {
private final double r;
public Circle(double r) { this.r = r; }
@Override public double area() { return Math.PI * r * r; }
@Override public double perimeter() { return 2 * Math.PI * r; }
}Если класс реализует интерфейс, но не предоставляет все методы, класс должен быть объявлен abstract — то же правило, что и для абстрактных классов.
Также можно реализовывать сразу несколько интерфейсов:
public class Money implements Comparable<Money>, java.io.Serializable {
private final long cents;
public Money(long cents) { this.cents = cents; }
public int compareTo(Money other) { return Long.compare(this.cents, other.cents); }
}Это «множественное наследование типа» — Money является одновременно Comparable<Money> и Serializable. Код, которому нужен любой из них, может принимать Money.
Программирование к интерфейсу
Смысл интерфейсов — писать код против контракта, а не против реализации:
public double sumAreas(List<Shape> shapes) {
double sum = 0;
for (Shape s : shapes) sum += s.area();
return sum;
}sumAreas ничего не знает и не заботится о Circle. Добавьте Square implements Shape, Triangle implements Shape, и функция будет работать и со списками из них — без изменений.
Стандартная библиотека построена на этом паттерне. Почти всегда переменную объявляют с типом интерфейса и создают конкретный экземпляр:
List<String> names = new ArrayList<>(); // List, not ArrayList
Map<String, Integer> counts = new HashMap<>(); // Map, not HashMapЕсли позднее вы переключитесь на LinkedList или LinkedHashMap, изменится только строка с new.
Константы в интерфейсах
Каждое поле, объявленное в интерфейсе, неявно является public static final — константой. Обычно константы в интерфейсах не добавляют (это считается плохим стилем — Constant Interface Antipattern), но синтаксис существует:
public interface Color {
String DEFAULT = "black"; // implicitly public static final
}Если нужны константы, предпочтите перечисление или обычный final class с полями public static final.
Интерфейсы могут расширять интерфейсы
Интерфейс может расширять другие интерфейсы — даже несколько сразу:
public interface Readable { String read(); }
public interface Writable { void write(String s); }
public interface ReadWrite extends Readable, Writable { }Теперь любой класс, реализующий ReadWrite, должен предоставить и read(), и write(). Здесь нет различия между class extends и interface implements — интерфейсы просто расширяют интерфейсы с помощью extends.
Методы по умолчанию и статические методы (обзор)
Начиная с Java 8, интерфейсы могут иметь методы по умолчанию (тела предоставляются через ключевое слово default) и static-методы. Они позволяют добавлять поведение к интерфейсу, не ломая все существующие реализации. Подробный разбор — в следующей главе, методы по умолчанию:
public interface Shape {
double area();
// Default method — implementors get this for free.
default String describe() {
return getClass().getSimpleName() + " area=" + area();
}
// Static factory on the interface itself.
static Shape unitCircle() { return new Circle(1); }
}Маркерные интерфейсы
Маркерный интерфейс не объявляет никаких методов. Он существует исключительно как метка, которую может проверить код во время выполнения:
public interface Cacheable { } // no methods
public class Snapshot implements Cacheable { ... }А затем где-то ещё: if (obj instanceof Cacheable) { ... }. Современная Java предпочитает аннотации для подобных метаданных (@Cacheable вместо implements Cacheable), но Serializable, Cloneable и RandomAccess — хорошо известные маркерные интерфейсы в стандартной библиотеке.
Функциональные интерфейсы
Интерфейс ровно с одним абстрактным методом является функциональным интерфейсом. Это важно, потому что Java позволяет передавать такой интерфейс через лямбда-выражение или ссылку на метод вместо полного анонимного класса — лямбда является реализацией этого единственного метода.
@FunctionalInterface
public interface Transformer {
String apply(String input); // the single abstract method
}
Transformer upper = s -> s.toUpperCase(); // lambda implements apply
System.out.println(upper.apply("hi")); // prints: HIНеобязательная аннотация @FunctionalInterface — защита на этапе компиляции: код не скомпилируется, если у интерфейса окажется более одного абстрактного метода. (Методы по умолчанию и статические методы в этот лимит не входят.) Стандартная библиотека поставляется с целым набором таких интерфейсов — Runnable, Comparator, Function, Predicate, Supplier — рассмотренных в разделе функциональные интерфейсы.
Выбор между интерфейсом и абстрактным классом
Дерево решений:
- Нужно ли подклассам разделять состояние или общие реализации методов? Если да, вероятно, нужен абстрактный класс — интерфейсы не могут хранить состояние экземпляра.
- Хотите ли вы тип, которому могут удовлетворять несколько не связанных между собой классов? Если да, интерфейс — класс может реализовывать много интерфейсов, но расширять только один класс.
- Будет ли контракт расти со временем? Интерфейсы эволюционируют осторожнее — добавление абстрактного метода в интерфейс ломает все реализации, если только не сделать его
default-методом. Абстрактные классы могут добавлять конкретный метод, не ломая никого.
В большинстве реальных кодовых баз интерфейсы побеждают для типовых контрактов, а абстрактные классы появляются за кулисами, когда несколько реализаций разделяют инфраструктуру.
Рабочий пример
Что дальше
Раньше интерфейсы были чистыми контрактами — без тел методов вообще. Начиная с Java 8 это изменилось: интерфейсы могут предоставлять методы по умолчанию, static-методы и даже private-вспомогательные методы. Следующая глава — обзор этих дополнений. Продолжите с методами по умолчанию.