Full Stack Blog – C# vs Java. Интерфейсы в Java

02 April 2023

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

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

Интерфейс в 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# будут описаны в следующей.