Skip to main content

C# vs Java. Интерфейсы в Java

· 8 min read
Andrey Ganyushkin

Интерфейс - контракт (набор методов и констант), описывающий какие есть способы взаимодействовать с объектом, который реализует этот контракт. С другой стороны, тот кто заявляет о поддержке какого-либо контракта обязан реализовать все методы описанные в контракте.

Интерфейс в Java позволяет реализовать полиморвизм и множественное наследование.

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 запрещена.

статические методы

Статические методы считаются публичными по умолчанию.

поля

Интерфейсы могут содержать поля, так же как и обычные классы, но с несколькими отличиями:

  • Поля должны быть проинициализированы
  • Поля считаются публичными, статическими и финальными
  • Модификаторы public, static и 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()
}
}

Преобразование типов

cast object to interface

Здесь нет никаких проблем. Мы просто преобразовываем объект из типа класса к типу интерфейса и используем его.

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());

cast 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'

Никаких дополнительных действий, просто присваиваим ссылку новой переменной с другим типом.

Java Records и интерфейсы?

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#

Статья получается слишком большой. Интерфейсы в C# будут описаны в следующей.