В следующем коде объясните, почему вызов A.super.m не компилируется, хотя B наследует метод от A: пример с ...

В следующем коде объясните, почему вызов A.super.m() не компилируется, хотя B наследует метод от A:

interface A {
    default void m() {
        System.out.println("A");
    }
}

interface B extends A {
}

class C implements B {
    void call() {
        A.super.m();
    }
}
Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

A.super.m() запрещён, потому что A не является непосредственным суперинтерфейсом класса C; непосредственный суперинтерфейс — только B. Для вызова унаследованной реализации нужно написать B.super.m().

Квалифицированный вызов Интерфейс.super.метод() разрешён только для прямого суперинтерфейса текущего класса. В данном случае B.super.m() фактически вызовет реализацию m, унаследованную B от A.

Исторический контекст

Механизм default-методов появился в Java 8, чтобы интерфейсы могли получать новые методы с реализацией без немедленной поломки всех существующих классов-реализаторов. Вместе с этим потребовался явный способ обратиться к реализации интерфейса, если класс хочет использовать её напрямую, а не обычный виртуальный вызов.

Синтаксис Интерфейс.super.метод() решает эту задачу для непосредственного суперинтерфейса. Ограничение защищает иерархию интерфейсов: подкласс не должен произвольно обходить более специфичный уровень и вызывать реализацию далёкого предка.

Постановка проблемы

B является подинтерфейсом A, поэтому метод m доступен классу C через B. Однако доступность метода и возможность квалифицированного вызова — разные правила.

Если разрешить A.super.m() в таком коде, C смог бы явно обойти возможную переопределённую default-реализацию в B. Это создало бы неоднозначную семантику: класс объявляет реализацию через B, но принудительно выбирает более старый уровень иерархии.

Подробное решение

В выражении Интерфейс.super.m() имя интерфейса обозначает не любой предок, а непосредственный суперинтерфейс текущего класса. У C список непосредственных суперинтерфейсов содержит B, поэтому допустим следующий вызов:

interface A { default void m() { System.out.println("A"); } } interface B extends A { } class C implements B { void call() { B.super.m(); // печатает A } }

Поскольку B не объявляет собственную реализацию, его default-метод наследуется от A, и B.super.m() приводит к вызову этой реализации. Если B переопределит m, тот же вызов начнёт использовать реализацию B.

Это не обычный полиморфный вызов через объект. Вызов через B.super явно выбирает реализацию, связанную с интерфейсом B, поэтому переопределение m в классе C не будет вызвано:

interface A { default void m() { System.out.println("A"); } } class C implements A { public void m() { System.out.println("C"); } void call() { A.super.m(); } // печатает A }

Ограничение действует на этапе компиляции. Если интерфейс не является непосредственным суперинтерфейсом, компилятор отклоняет квалификатор A.super, даже если метод m фактически унаследован через него.

Ситуация из практики

Предположим, библиотечный интерфейс BaseLogger содержит default-метод log, а прикладной интерфейс ServiceLogger расширяет его и позже добавляет собственное форматирование. Класс сервиса реализует ServiceLogger и хочет явно выбрать базовую реализацию.

Вариант BaseLogger.super.log() не компилируется: класс непосредственно связан с ServiceLogger, а не с BaseLogger. Вариант ServiceLogger.super.log() корректен и автоматически использует наиболее специфичную default-реализацию, доступную через этот прямой интерфейс.

Можно также вызвать обычный log() или this.log(). Это поддерживает полиморфизм и учитывает переопределение метода в классе, но не гарантирует выбор именно default-реализации интерфейса. Поэтому для намеренного обхода реализации класса подходит ServiceLogger.super.log(), а для обычного поведения — виртуальный вызов log().

Такой выбор делает зависимость явной: Интерфейс.super следует применять только когда классу действительно нужна конкретная реализация интерфейса, а не полиморфное поведение.

Что кандидаты часто упускают

  1. Что произойдёт, если B переопределит m?

    При вызове B.super.m() будет выбрана реализация B, а не A. Квалификатор указывает на непосредственный интерфейс B, и разрешение default-метода начинается с него. Обращение к A.super.m() по-прежнему останется недопустимым.

  2. Вызывает ли B.super.m() переопределение m в классе C?

    Нет. Это специальный вызов реализации суперинтерфейса, а не вызов через ссылку на текущий объект. Если написать обычный m(), будет применён динамический диспетчинг и вызван метод C, если он его переопределяет; B.super.m() этот механизм намеренно обходит.

  3. Можно ли вызвать B.super.m(), если B не содержит default-метод напрямую?

    Да, если m доступен в B как унаследованный default-метод. В исходном примере B не объявляет m, но наследует его от A, поэтому B.super.m() корректен и вызывает реализацию A. Если же B переобъявит m как абстрактный метод, конкретный класс должен предоставить реализацию, а вызов B.super.m() станет недопустимым, поскольку у B не будет доступной default-реализации.