Перейти к содержимому
Теория · Интерфейсы, enum и record · урок 3 из 8

default-методы

В прошлом уроке класс, который реализует интерфейс, писал каждый его метод сам. Из-за этого правила интерфейс трудно менять.

Вернёмся к объявлению о работе. Допустим, работодатель дописал в него новую обязанность: после доставки отчитаться. Все курьеры, которые уже взяли работу, разом нарушают договор, ведь этого они не умеют. В Java то же самое: добавьте в Courier новый метод без тела, и каждый класс, который реализует Courier, перестанет компилироваться.

Работодатель поступает умнее и прикладывает к новой обязанности готовую инструкцию: «Если у вас нет своего способа, напишите в чат „Delivered“». Кто умеет отчитываться по-своему, так и делает, а остальные действуют по инструкции. В Java такая инструкция — default-метод. Чтобы следить только за отчётом, оставим в Courier один метод — report:

JAVA
public class Main {
    public static void main(String[] args) {
        Courier c = new Cyclist();
        c.report(); // Delivered
    }
}

interface Courier {
    default void report() {
        System.out.println("Delivered");
    }
}

class Cyclist implements Courier {}

Давайте разберём программу по шагам:

  1. В интерфейсе Courier метод report написан с телом, а перед ним стоит слово default.
  2. Класс Cyclist реализует Courier, но своего report у него нет, и компилятор не возражает.
  3. c.report() выполняет тело из интерфейса и печатает Delivered.

Метод с телом в интерфейсе

default-метод — метод интерфейса с телом и словом default перед ним. Такие методы появились в Java 8.

Что важно знать о default-методах:

  • класс получает default-метод готовым и не обязан писать его сам;
  • default-метод, как и метод без тела, public;
  • класс может написать свою версию: тогда он переопределяет default-метод, как метод родителя в прошлой теме.

На собеседовании часто спрашивают, зачем появились default-методы. Ответ: чтобы добавлять в интерфейс новые методы и не ломать классы, которые его уже реализуют.

Какой метод выполнится

Правило одно. При вызове Java сначала ищет метод в классе объекта, а потом у его родителей. Только если никто из них не написал свой метод, выполняется default-метод интерфейса. Курьер, у которого есть свой способ отчитаться, инструкцию не читает.

В виджете два курьера: у Cyclist нет своего report, а у Drone есть. Нажимайте «Следующий шаг» и следите, чья рамка ложится сверху при каждом вызове.

ВживуюЧей report выполнитсяшаг 1 из 7 · рамок: 1

a ведёт к объекту Cyclist, а в классе Cyclist своего report нет.

Верхняя рамка — метод, который работает сейчас

Оба вызова записаны одинаково, через переменную типа Courier. Какой report выполнить, решил объект: для Cyclist нашлась только инструкция из интерфейса, а Drone отчитался по-своему. Это тот же полиморфизм, что и с универсальным пультом.

Здесь картинка с инструкцией неточна. На бумажной инструкции курьер может отмечать, сколько отчётов он уже отправил. У default-метода такой бумажки нет: интерфейс не хранит данных объекта. Об этом — следующий раздел.

Абстрактный класс или интерфейс

Теперь методы с телом бывают и у интерфейса, и у абстрактного класса. Чем же они различаются? Это один из самых частых вопросов на собеседовании по Java.

Абстрактный классИнтерфейс
Поля объектаесть, у каждого объекта свои значениянет, поля интерфейса — общие константы
Конструкторестьнет
Сколько может взять классодного родителя через extendsсколько угодно через implements

За таблицей стоит разный смысл:

  • абстрактный класс отвечает на вопрос «что это?»: велосипедист и водитель фургона — сотрудники, и у каждого есть имя и число доставок;
  • интерфейс отвечает на вопрос «что умеет?»: доставлять умеет и сотрудник, и дрон.

Отсюда правило выбора. Если родственным классам нужны общие поля и конструктор, берите абстрактный класс. Если умение получат совсем разные классы, берите интерфейс. Класс может сочетать оба: class Van extends Vehicle implements Courier.

Частая ошибка

Число отчётов попытались хранить в поле интерфейса:

JAVA
interface Courier {
    int reports = 0;
    default void report() {
        reports++; // ✗
        System.out.println("Delivered");
    }
}
TEXT
error: cannot assign a value to static final variable reports

Программа не компилируется. Поле в интерфейсе — константа: слова static и final компилятор добавил сам, и сообщение называет оба. Хранить данные объекта, например число его отчётов, интерфейс не может. Такое поле объявляют в классе, а если оно нужно нескольким родственным классам — в их общем абстрактном родителе.

Главное

  • default-метод — метод интерфейса с телом: класс получает его готовым и может переопределить.
  • При вызове Java сначала ищет метод в классе объекта и его родителях, и default-метод выполняется, только если там своего нет.
  • У абстрактного класса есть поля объекта и конструктор, а поля интерфейса — общие константы; зато интерфейсов класс может реализовать сколько угодно.
  • Абстрактный класс описывает, что это за объект, а интерфейс — что он умеет.