Статьи

Методы Java 8 по умолчанию могут нарушить ваш (пользовательский) код

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

Все идет нормально. Однако добавление методов по умолчанию к установленным интерфейсам может привести к невозможности компиляции кода Java. Это проще всего понять, глядя на пример. Давайте предположим, что в качестве входных данных для библиотеки требуется класс одного из ее интерфейсов:

interface SimpleInput {
  void foo();
  void bar();
}
 
abstract class SimpleInputAdapter implements SimpleInput {
  @Override
  public void bar() {
    // some default behavior ...
  }
}

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

Предположим также, что пользователь использовал этот адаптер:

class MyInput extends SimpleInputAdapter {
  @Override
  public void foo() {
    // do something ...
  }
  @Override
  public void bar() {
    super.bar();
    // do something additionally ...
  }
}

С этой реализацией пользователь может наконец взаимодействовать с библиотекой. Обратите внимание, как реализация переопределяет метод bar, чтобы добавить некоторые функции в реализацию по умолчанию.

Так что же произойдет, если библиотека перейдет на Java 8? Прежде всего, библиотека, скорее всего, устареет от класса адаптера и перенесет функциональность в методы по умолчанию. В результате интерфейс теперь будет выглядеть так:

interface SimpleInput {
  void foo();
  default void bar() {
    // some default behavior
  }
}

Благодаря этому новому интерфейсу пользователь может обновить свой код, чтобы адаптировать метод по умолчанию вместо использования класса адаптера. Самое замечательное в использовании интерфейсов вместо классов адаптеров — это возможность расширить класс, отличный от конкретного адаптера. Давайте приведем это в действие и перенесем MyInputкласс для использования метода по умолчанию. Поскольку теперь мы можем расширять другой класс, давайте дополнительно расширим некоторый сторонний базовый класс. То, что делает этот базовый класс, здесь не имеет особого значения, поэтому давайте просто предположим, что это имеет смысл для нашего варианта использования.

class MyInput extends ThirdPartyBaseClass implements SimpleInput {
  @Override
  public void foo() {
    // do something ...
  }
  @Override
  public void bar() {
    SimpleInput.super.bar();
    // do something additionally ... 
  }
}

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

Библиотека, которую мы используем, имеет большой успех. Однако сопровождающему необходимо добавить другой интерфейс, чтобы предложить больше функциональности. Этот интерфейс представляет собой , ComplexInputкоторый расширяет SimpleInputс дополнительным методом. Поскольку методы по умолчанию, как правило, считаются безопасными для добавления , сопровождающий дополнительно переопределяет SimpleInputметод по умолчанию и добавляет некоторое поведение, чтобы предложить лучшее значение по умолчанию. В конце концов, это было довольно распространено при реализации классов адаптеров:

interface ComplexInput extends SimpleInput {
  void qux();
  @Override
  default void bar() {
    SimpleInput.super.bar(); 
    // so complex, we need to do more ...
  }
} 

Эта новая функция оказывается настолько великолепной, что разработчик ThirdPartyBaseClassрешил также положиться на эту библиотеку. Для выполнения этой работы он реализует ComplexInputинтерфейс для ThirdPartyLibrary.

But what does that mean to the MyInput class? Due to the implicit implementation of ComplexInput by extending ThirdPartyBaseClass, calling the default method of SimpleInput has suddenly become illegal. As a result, the user’s code does not longer compile. Also, it is now generally forbidden to call this method since Java sees this call as illegal as calling a super-super method of an indirect super class. Instead, you could call the default method of the ComplexInput class. However, this would require you to first explicitly implement this interface in MyInput. For the user of the library, this change comes most likely rather unexpected!

Oddly enough, the Java runtime does not make this distinction. The JVM’s verifier will allow a compiled class to call SimpleInput::foo even if the loaded class at run time implicitly implements the ComplexClass by extending the updated version of ThirdPartyBaseClass. It is only the compiler that complains here.

But what do we learn from this? In a nutshell, make sure to never override a default method in another interface. Neither with another default method, nor with an abstract method. In general, be careful about using default methods at all. As much as they ease the evolution of established APIs as Java’s collection interfaces, they are intrinsically complex by allowing to perform method invocations that go sideways in your type hierarchy. Before Java 7, you would only need to look for the actually invoked code by traversing down a linear class hierarchy. Only add this complexity when you really feel it is absolutely necessary.