Интерфейс - контракт (набор методов и констант), описывающий какие есть способы взаимодействовать с объектом, который реализует этот контракт. С другой стороны, тот кто заявляет о поддержке какого-либо контракта обязан реализовать все методы описанные в контракте.
Интерфейс в Java позволяет реализовать полиморвизм и множественное наследование.
Сначала рассмотрим как интерфейсы устроены в Java.
Не нужно стесняться использовать имя
IPlant
для интерфейсов ;).
Но, чтобы не нервировать народ из Java мира в Java коде будем использоватьPlantInterface
.
interface PlantInterface {
String getName();
}
Тут все просто. У нас есть сущность Plant и мы хотим чтобы она предоставляла возможность просто получить имя растения.
@Getter
@RequiredArgsConstructor
class Plant implements PlantInterface {
private final String ruName;
private final String latName;
private final String enName;
public String getName() {
if (latName != null) {
return latName;
} else if (enName != null) {
return enName;
} else {
return ruName
}
}
}
Немного Lombok, чтобы оставить только важное.
Множественное наследование в Java запрещено и если у нас есть классы BasePlant
и PlantUtils
, то мы не можем сделать так:
class Plant extends BasePlant,PlantUtils { // WRONG!
// ...
}
Это запрещено чтобы исключить следующий проблемы diamond problem.
В Java разрешено реализовывать несколько интерфейсов в одном классе и вместе со статическими методами и методами по умолчанию в интерфейсах, речь о которых пойдет ниже, возвращаюется возможность нахватать ошибок с множественным наследованием.
В общем-то есть основная идея - в интерфейсе мы определяем контракт, а это значит, что класс реализующий интерфейс должен имплементировать его как public методы. Но, как водится, с некоторыми особенностями.
Методы в интерфейсах считаются публичными и абстрактными.
Методы не могут быть финальными ;) abstract и final запрещена.
Статические методы считаются публичными по умолчанию.
Интерфейсы могут содержать поля, так же как и обычные классы, но с несколькими отличиями:
Cчитаются публичными и статическими.
public interface MyInterface {
class MyClass {
//...
}
interface MyOtherInterface {
//...
}
}
Предоставляется возможность создавать обобщенные интерфейсы. Пример ниже илюстрирует эту возможность.
// Обобщенный интерфейс. Параметризовать типом T
interface Box<T> {
void insert(T item);
}
// Реализовать интерфейс можно вот так, указав с каким типом мы хотели бы работать
class ShoeBox implements Box<Shoe> {
public void insert(Shoe item) {
//...
}
}
Про дженерики отдельно можно посмотреть здесь.
При необходимости мы можем расширить интерфейс дополнительным контрактом
interface PlantInterface {
String getName();
}
// сделаем отдельный контракт для деревьев
interface TreeInterface extends PlantInterface {
bool isConiferous();
}
// и для цветов
interface FlowerInterface extends PlantInterface {
Color getColor();
}
// Реализовать это можно следующим образом
class Tree implements TreeInterface {
public String getName() {
// ...
}
public bool isConiferous() {
// ...
}
}
class Tree implements FlowerInterface {
public String getName() {
// ...
}
public Color getColor() {
// ...
}
}
Статические методы в интерфейсах не наследуются. Такой метод можно использоват только так PlantInterface.formatName(plantObject)
Решает некоторые проблемы множественного наследования при имплементации нескольких интерфейсов, имеющих статический метод (сигнатура + реализация) с одинаковой сигнатурой.
Статические методы могут использоваться как замена Util классам, реализуя необходимые методы работы с описываемым объектом.
interface PlantInterface {
String getName();
static String formatName(PlantInterface plant) {
return "PLANT: " + plant.getName()
}
}
Статические поля наследуются и могут дыть использованы как член инстанса сласса. Можно использовать так plantObject.SEARCH_WEIGHT
или так PlantInterface.SEARCH_WEIGHT
interface PlantInterface {
public static final float SEARCH_WEIGHT = 0;
String getName();
}
Учитывая это "Модификаторы public, static и final не нужно указывать явно (они «проставляются» по умолчанию)" мы можем написать так:
interface PlantInterface {
float SEARCH_WEIGHT = 0;
String getName();
}
Методы по умолчанию - это сигнатура + тело (реализация) метода. Такой метод располагается в интерфейсе и этот мето не статический.
Зачем это нужно: через интерфейсы мы можем предоставлять реализацию некоторым методов и если это понадобится менять эту реализацию, а тамже добавлять методы объектам и всё это не ломая обратной совместимости.
Ну или - это было сделанно в перпую очередь чтобы обеспечить возможность реализовать Stream API не сломав обратную совместимость.
Выглядит это так:
interface PlantInterface {
String getName();
default String formatName() {
return "PLANT: " + this.getName();
}
}
Методы по умолчанию наследуются и класс может переопределить реализацию метода по умолчанию.
// Base реализация интерфейса
class Plant implements PlantInterface {
@Override
public String getName() {
return "common plant object";
}
}
new Plant().formatName(); // вернет - "PLANT: common plant object"
// Реализация интерфейса с переопределением метода по умолчанию
class Tree implements PlantInterface {
@Override
public String getName() {
return "Betula Pendula";
}
@Override
public String formatName() {
return "TREE: " + this.getName();
}
}
new Tree().formatName(); // вернет - "TREE: Betula Pendula"
Запрещено реализовывать методы по умолчанию с сигнатурой методов toString, equals и hashCode класса Object. Получим ошибку: "Default method 'toString' overrides a member of 'java.lang.Object'"
So simple to understand: the methods from Object -- such as toString, equals, and hashCode -- are all about the object's state. But interfaces do not have state; classes have state.
Если при моножественном наследовании мы получаем ошибку компиляции из-за наличия двух одинаковых сигнатур в разных интерфейсах то мы можем переопределить метод с такой сигнатурой и предоставить свою реализацию, в которой обращаться к методам в интерфейсах. Пример:
public class ChildClass implements A, C {
@Override
public void foo() {
//you could completely override the default implementations
doSomethingElse();
//or manage conflicts between the same method foo() in both A and C
A.super.foo();
}
public void bah() {
A.super.foo(); //original foo() from A accessed
C.super.foo(); //original foo() from C accessed
}
}
Да. Приватные методы в интерфейсе...
interface PlantInterface {
String getName();
default String formatName() {
return formatNameWithPrefix();
}
private String formatNameWithPrefix() {
return "PLANT: " + this.getName()
}
}
или для варианта со статичскими методами
interface PlantInterface {
String getName();
static String formatName(PlantInterface plant) {
return formatNameWithPrefix(PlantInterface plant);
}
private static String formatNameWithPrefix(PlantInterface plant) {
return "PLANT: " + plant.getName()
}
}
Здесь нет никаких проблем. Мы просто преобразовываем объект из типа класса к типу интерфейса и используем его.
interface PlantInterface {
String getName();
}
class Tree implements PlantInterface {
private String name;
public Tree(String name) {
this.name = name;
}
@Override
public String getName() {
return name;
}
}
// in code
var tree = new Tree("Betula pendula Roth");
var plant = (PlantInterface) tree; // works fine!
System.out.println(plant.getName());
List<Tree\>
to List<PlantInterface\>
interface PlantInterface {
String getName();
}
class Tree implements PlantInterface {
private String name;
public Tree(String name) {
this.name = name;
}
@Override
public String getName() {
return name;
}
}
// in code
var treeList = new ArrayList<Tree>() {{
add(new Tree("Betula pendula Roth"));
add(new Tree("Picea glauca var. albertiana 'Alberta Globe'"));
add(new Tree("Populus x generosa 'Barn'"));
}};
ArrayList<PlantInterface> plantList = (ArrayList<PlantInterface>) treeList; // ERROR!
plantList.stream()
.map(PlantInterface::getName)
.forEach(System.out::println);
Мы не можем напрямую преобразовать список с объектами к списку List<PlantInterface>
"Inconvertible types; cannot cast java.util.ArrayList<Main.Tree>
to java.util.ArrayList<Main.PlantInterface>
". Поэтому нужно сделать что-то подобное:
// ...
List<PlantInterface> plantList = treeList.stream()
.map(plant -> (PlantInterface) plant)
.toList();
plantList.stream()
.map(PlantInterface::getName)
.forEach(System.out::println);
// output:
// Betula pendula Roth
// Picea glauca var. albertiana 'Alberta Globe'
// Populus x generosa 'Barn'
Есть более красивый способ преобразовать ArrayList<Tree>
в ArrayList<PlantInterface>
или List<PlantInterface>
.
Делается это с "правильным" :) использованием дженериков вот так:
ArrayList<Tree> treeList = ...
// with List
List<? extends PlantInterface> plantList = treeList;
// or with ArrayList
ArrayList<? extends PlantInterface> plantList = treeList;
plantList.stream()
.map(PlantInterface::getName)
.forEach(System.out::println);
// output:
// Betula pendula Roth
// Picea glauca var. albertiana 'Alberta Globe'
// Populus x generosa 'Barn'
Никаких дополнительных действий, просто присваиваим ссылку новой переменной с другим типом.
Records могут реализовывать интерфейсы аналочигно классам
interface PlantInterface {
String getName();
}
record Tree(String name) implements PlantInterface {
@Override
public String getName() {
return this.name;
}
}
// in code
var treeList = new ArrayList<Tree>() {{
add(new Tree("Betula pendula Roth"));
add(new Tree("Picea glauca var. albertiana 'Alberta Globe'"));
add(new Tree("Populus x generosa 'Barn'"));
}};
// это работает даже при подобных приведениях типов
List<? extends PlantInterface> plantList = treeList;
plantList.stream()
.map(PlantInterface::getName)
.forEach(System.out::println);
// output:
// Betula pendula Roth
// Picea glauca var. albertiana 'Alberta Globe'
// Populus x generosa 'Barn'
Статья получается слишком большой. Интерфейсы в C# будут описаны в следующей.