Интерфейс в Java сильно эволюционировал за прошедшие годы. Давайте рассмотрим, какие изменения произошли в процессе его развития.
Оригинальные интерфейсы
Интерфейсы в Java 1.0 были достаточно простыми по сравнению с тем, какие они сейчас. Они могли содержать лишь два типа элементов: константы и публичные абстрактные методы.
Поля-константы
Интерфейсы могут содержать поля, так же как и обычные классы, но с несколькими отличиями:
- Поля должны быть проинициализированы
- Поля считаются публичными статическими финальными
- Модификаторы public, static и final не нужно указывать явно (они «проставляются» по умолчанию)
public interface MyInterface {
int MY_CONSTANT = 9;
}
Даже несмотря на то, что явно это не задано, поле MY_CONSTANT считается публичной статической финальной константой. Вы можете добавить эти модификаторы, но делать это не обязательно.
Абстрактные методы
Наиболее важными элементами интерфейса являются его методы. Методы интерфейса также отличаются от методов обычного класса:
- У методов нет тела
- Реализация методов предоставляется классами, реализующими данный интерфейс
- Методы считаются публичными и абстрактными даже, если это не задано явно
- Методы не могут быть финальными, поскольку в Java комбинация модификаторов abstract и final запрещена
public interface MyInterface {
int doSomething();
String doSomethingCompletelyDifferent();
}
Вложенность
Java 1.1 представила концепцию классов, которые можно размещать внутри других классов. Такие классы бывают двух видов: статические и нестатические. Интерфейсы так же могут содержать внутри себя другие интерфейсы и классы.
Даже если это не задано явно, такие интерфейсы и классы считаются публичными и статическими.
public interface MyInterface {
class MyClass {
//...
}
interface MyOtherInterface {
//...
}
}
Перечисления и аннотации
В Java 5 были введены ещё два типа: Перечисления и Аннотации. Они также могут быть помещены внутрь интерфейсов.
public interface MyInterface {
enum MyEnum {
FOO, BAR;
}
@interface MyAnnotation {
//...
}
}
Обобщенные типы
Java 5 ввела концепцию «дженериков» — обобщенных типов. Вкратце: «дженерики» позволяют вам использовать обобщенный тип вместо указания конкретного типа. Таким образом, вы можете писать код, который работает с различным количеством типов, не жертвуя при этом безопасностью и не предоставляя отдельную реализацию для каждого типа.
В интерфейсах, начиная с Java 5, вы можете определить обобщенный тип, а затем использовать его в качестве типа возвращаемого значения метода или в качестве типа аргумента метода.
Интерфейс Box работает независимо от того, используете ли вы его для хранения объектов типа String, Integer, List, Shoe или каких-либо других.
interface Box<T> {
void insert(T item);
}
class ShoeBox implements Box<Shoe> {
public void insert(Shoe item) {
//...
}
}
Статические методы
Начиная с Java 8, вы можете включать в интерфейсы статические методы. Данный подход изменил привычный для нас способ работы интерфейса. Они теперь работают совсем не так, как работали до Java 8. Первоначально все методы в интерфейсах были абстрактными. Это означало, что интерфейс предоставлял лишь сигнатуру, но не реализацию. Реализация оставалась за классами, реализующими ваш интерфейс.
При использовании статических методов в интерфейсах вам нужно также предоставить реализацию тела метода. Чтобы задействовать в интерфейсе такой метод, просто используйте ключевое слово static. Статические методы считаются публичными по умолчанию.
public interface MyInterface {
// This works
static int foo() {
return 0;
}
// This does not work,
// static methods in interfaces need body
static int bar();
}
Наследование статических методов
В отличие от обычных статических методов, статические методы в интерфейсах не наследуются. Это означает, что если вы хотите вызвать такой метод, вы должны вызвать его напрямую из интерфейса, а не из реализующего его класса.
MyInterface.staticMethod();
Это поведение очень полезно для избежания проблем при множественном наследовании. Представьте, что у вас есть класс, реализующий два интерфейса. У каждого из интерфейсов есть статический метод с одинаковым именем и сигнатурой. Какой из них должен быть использован в первую очередь?
Почему это полезно
Представьте, что у вас есть интерфейс и целый набор вспомогательных методов, которые работают с классами, реализующими этот интерфейс.
Традиционно существовал подход в использовании класса-компаньона. В дополнение к интерфейсу создавался утилитный класс с очень похожим именем, содержащий статические методы, принадлежащие интерфейсу.
Вы можете найти примеры использования данного подхода прямо в JDK: интерфейс java.util.Collection и сопутствующий ему утилитный класс java.util.Collections.
Со статическими методами в интерфейсах этот подход больше не актуален, не нужен и не рекомендован. Теперь вы можете иметь все в одном месте.
Методы по умолчанию
Методы по умолчанию похожи на статические методы тем, что для них вы также должны предоставлять тело. Чтобы объявить метод по умолчанию, просто используйте ключевое слово default.
public interface MyInterface {
default int doSomething() {
return 0;
}
}
В отличие от статических методов, методы по умолчанию наследуются классами, реализующими интерфейс. Что важно, такие классы могут при необходимости переопределять их поведение.
Хотя есть одно исключение. В интерфейсе не может быть методов по умолчанию с такой же сигнатурой, как у методов toString, equals и hashCode класса Object. Взгляните на ответ Брайана Гетца, чтобы понять обоснованность такого решения: Allow default methods to override Object's methods.
Почему это полезно
Идея с реализацией методов прямо в интерфейсе выглядит не совсем правильной. Так почему в первую очередь была введена эта функциональность?
У интерфейсов есть одна проблема. Как только вы предоставите свой API другим людям, он навсегда «окаменеет» (его нельзя будет изменить безболезненно).
По традиции, Java очень серьезно относится к обратной совместимости. Методы по умолчанию предоставляют способ расширить существующие интерфейсы новыми методами. Наиболее важно то, что методы по умолчанию уже предоставляют определенную реализацию. Это означает, что классам, реализующим ваш интерфейс, не нужно реализовывать какие-либо новые методы. Но, если в этом будет необходимость, методы по умолчанию можно будет переопределить в любое время, если их реализация перестанет подходить. Таким образом, вкратце, вы можете предоставить новую функциональность существующим классам, реализующим ваш интерфейс, сохраняя при этом совместимость.
Конфликты
Давайте представим, что у нас есть класс, реализующий два интерфейса. У этих интерфейсов есть метод по умолчанию с одинаковыми именем и сигнатурой.
interface A {
default int doSomething() {
return 0;
}
}
interface B {
default int doSomething() {
return 42;
}
}
class MyClass implements A, B {
}
Теперь один и тот же метод по умолчанию с одной и той же сигнатурой унаследован от двух разных интерфейсов. У каждого интерфейса своя реализация этого метода.
Итак, как наш класс узнает, какую из двух различных реализаций использовать?
Он и не узнает. Приведенный выше код приведет к ошибке компиляции. Если вам требуется заставить его работать, то для этого необходимо переопределить конфликтный метод в вашем классе.
interface A {
default int doSomething() {
return 0;
}
}
interface B {
default int doSomething() {
return 42;
}
}
class MyClass implements A, B {
// Without this the compilation fails
@Override
public int doSomething() {
return 256;
}
}
Приватные методы
С появлением Java 8 и введением методов по умолчанию и статических методов, у интерфейсов появилась возможность содержать не только сигнатуры методов, но и их реализации. При написании таких реализаций рекомендуется разделять сложные методы на более простые. Такой код легче переиспользовать, поддерживать и понимать.
Для такой цели вы бы использовали приватные методы, поскольку они могут содержать все детали реализации, которые не должны быть видимы и использованы извне.
К сожалению в Java 8 интерфейс не может содержать приватные методы. Это означает, что вы можете использовать:
- Длинные, сложные и трудные в понимании тела методов.
- Вспомогательные методы, которые являются частью интерфейса. Это нарушает принцип инкапсуляции и загрязняет публичный API интерфейса и классов-реализаций.
К счастью, начиная с Java 9, вы можете использовать приватные методы в интерфейсах. У них есть следующие особенности:
- у приватных методов есть тело, они не абстрактные
- они могут быть как статическими, так и нестатическими
- они не наследуются классами, реализующими интерфейс, и интерфейсами
- они могут вызывать другие методы интерфейса
- приватные методы могут вызывать другие приватные, абстрактные, статические методы или методы по умолчанию
- приватные статические методы могут вызывать только другие статические и приватные статические методы
public interface MyInterface {
private static int staticMethod() {
return 42;
}
private int nonStaticMethod() {
return 0;
}
}
Хронологический порядок
Ниже представлен хронологический перечень изменений по версиям Java:
Java 1.1
Вложенные классы
Вложенные интерфейсы
Java 5
Обобщенные типы
Вложенные перечисления
Вложенные аннотации
Java 8
Методы по умолчанию
Статические методы
Java 9
Приватные методы