W3docs

Интерфейсы 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 — рассмотренных в разделе функциональные интерфейсы.

Выбор между интерфейсом и абстрактным классом

Дерево решений:

  1. Нужно ли подклассам разделять состояние или общие реализации методов? Если да, вероятно, нужен абстрактный класс — интерфейсы не могут хранить состояние экземпляра.
  2. Хотите ли вы тип, которому могут удовлетворять несколько не связанных между собой классов? Если да, интерфейс — класс может реализовывать много интерфейсов, но расширять только один класс.
  3. Будет ли контракт расти со временем? Интерфейсы эволюционируют осторожнее — добавление абстрактного метода в интерфейс ломает все реализации, если только не сделать его default-методом. Абстрактные классы могут добавлять конкретный метод, не ломая никого.

В большинстве реальных кодовых баз интерфейсы побеждают для типовых контрактов, а абстрактные классы появляются за кулисами, когда несколько реализаций разделяют инфраструктуру.

Рабочий пример

java— editable, runs on the server

Что дальше

Раньше интерфейсы были чистыми контрактами — без тел методов вообще. Начиная с Java 8 это изменилось: интерфейсы могут предоставлять методы по умолчанию, static-методы и даже private-вспомогательные методы. Следующая глава — обзор этих дополнений. Продолжите с методами по умолчанию.

Практика

Практика
Какова практическая причина того, что Java не позволяет классу расширять более одного класса, но разрешает реализовывать много интерфейсов?
Какова практическая причина того, что Java не позволяет классу расширять более одного класса, но разрешает реализовывать много интерфейсов?
Was this page helpful?