Подтвердить что ты не робот

Представляет ли интерфейс метод по умолчанию интерфейс, который действительно сохраняет обратную совместимость?

Я думаю, что меня немного смущает введение методов по умолчанию для интерфейсов в Java. Насколько я понимаю, идея состоит в том, что методы по умолчанию могут быть введены в существующие интерфейсы без нарушения существующего кода.

Если я реализую интерфейс с не-абстрактным классом, я (конечно) должен определить реализации всех абстрактных методов в интерфейсе. Если интерфейс определяет метод по умолчанию, я наследую реализацию этого метода.

Если я реализую два интерфейса, я, очевидно, должен реализовать объединение абстрактных методов, определенных в обоих интерфейсах. Я наследую реализацию всех методов по умолчанию; однако, если произойдет столкновение между методами по умолчанию в двух интерфейсах, я должен переопределить этот метод в моем классе реализации.

Это звучит прекрасно, но как насчет следующего сценария?

Предположим, что есть интерфейс:

package com.example ;
/** 
* Version 1.0
*/
public interface A {
  public void foo() ;
  /**
  * The answer to life, the universe, and everything.
  */
  public default int getAnswer() { return 42 ;}
}

и второй интерфейс

package com.acme ;
/** 
* Version 1.0
*/
public interface B {
  public void bar() ;
}

Поэтому я могу написать следующее:

package com.mycompany ;
public class C implements com.example.A, com.acme.B {
  @Override
  public void foo() {
    System.out.println("foo");
  }
  @Override
  public void bar() {
    System.out.println("bar");
  }
  public static void main(String[] args) {
    System.out.println(new C().getAnswer());
  }
}

Итак, это должно быть хорошо, и действительно

java com.mycompany.C 

отображает результат 42.

Но теперь предположим, что acme.com делает следующее изменение для B:

package com.acme ;
/** 
* Version 1.1
*/
public interface B {
  public void bar() ;
  /**
  * The answer to life, the universe, and everything
  * @since 1.1
  */
  public default int getAnswer() {
    return 6*9;
  }
}

Как я понимаю, введение этого метода должно быть безопасным. Но если теперь я запустил существующий com.mycompany.C против новой версии, я получаю ошибку времени выполнения:

Exception in thread "main" java.lang.IncompatibleClassChangeError: Conflicting default methods: com/example/A.getAnswer com/acme/B.getAnswer
at com.mycompany.C.getAnswer(C.java)
at com.mycompany.C.main(C.java:12)

Это не совсем удивительно, но разве это не означает, что внедрение методов по умолчанию для существующих интерфейсов всегда сопряжено с риском нарушения существующего кода? Что мне не хватает?

4b9b3361

Ответ 1

Хотя добавление метода по умолчанию с тем же именем в обоих интерфейсах сделает код не скомпилированным, но как только вы разрешите ошибку компиляции, двоичные файлы, полученные после компиляции как интерфейсов, так и класса, реализующего интерфейсы, будут обратная совместимость.

Итак, совместимость действительно связана с бинарной совместимостью. Это объясняется в JLS & sect; 13.5.6 - Объявление метода интерфейса:

Добавление метода по умолчанию или изменение метода с абстрактного на по умолчанию, не нарушает совместимость с ранее существовавшими двоичными файлами, но может вызвать IncompatibleClassChangeError, если ранее существовавший двоичный файл пытается вызвать метод. Эта ошибка возникает, если type, T, является подтипом двух интерфейсов, я и J, где оба я и J объявить метод по умолчанию с той же сигнатурой и результатом и ни I, ни J не являются субинтерфейсом другого.

Другими словами, добавление метода по умолчанию - это двоично-совместимое изменение потому что он не вводит ошибки во время соединения, даже если это вводит ошибки во время компиляции или время вызова. На практике риск случайных столкновений, возникающих путем введения метода по умолчанию аналогичны тем, которые связаны с добавлением нового метода к не конечный класс. В случае столкновения, добавление метода в класс вряд ли вызовет LinkageError, но случайное переопределение метод в ребенке может привести к непредсказуемому поведению метода. И то и другое изменения могут привести к ошибкам во время компиляции.

Причина, по которой вы получили IncompatibleClassChangeError, вероятно, потому, что вы не перекомпилировали свой класс C после добавления метода по умолчанию в интерфейс B.

Также смотрите:

Ответ 2

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

например. вы можете исправить класс T следующим образом:

interface I {
    default void m() {}
}

interface J {
    default void m() {}
}

class T implements I, J {
    @Override
    public void m() { // forced override
        I.super.m(); // OK
    }
}

Все будет хорошо, пока не произойдет следующее изменение:

interface J extends I {
    @Override
    default void m() {}
}

Если перекомпилирован только интерфейс J, метод T::m по-прежнему будет делегировать I::m. Но компиляция T сама по себе больше не удастся — он потерпит неудачу с помощью error: bad type qualifier I in default super call:

class T implements I, J { // redundant I, but not an error
    @Override
    public void m() { // override not necessary, T::m resolves to J::m
        I.super.m(); // ERROR
    }
}