Статьи

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

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

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

01
02
03
04
05
06
07
08
09
10
11
interface SimpleInput {
  void foo();
  void bar();
}
 
abstract class SimpleInputAdapter implements SimpleInput {
  @Override
  public void bar() {
    // some default behavior ...
  }
}

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

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

01
02
03
04
05
06
07
08
09
10
11
class MyInput extends SimpleInputAdapter{
  @Override
  public void foo() {
    // do something ...
  }
  @Override
  public void bar() {
    super.bar();
    // do something additionally ...
  }
}

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

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

1
2
3
4
5
6
interface SimpleInput {
  void foo();
  default void bar() {
    // some default behavior
  }
}

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

01
02
03
04
05
06
07
08
09
10
11
class MyInput extends ThirdPartyBaseClass implements SimpleInput {
  @Override
  public void foo() {
    // do something ...
  }
  @Override
  public void bar() {
    SimpleInput.super.foo();
    // do something additionally ...
  }
}

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

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

1
2
3
4
5
6
7
8
interface ComplexInput extends SimpleInput {
  void qux();
  @Override
  default void bar() {
    SimpleInput.super.bar();
    // so complex, we need to do more ...
  }
}

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

Но что это значит для класса MyInput ? Из-за неявной реализации ComplexInput путем расширения ThirdPartyBaseClass , вызов метода SimpleInput по умолчанию внезапно стал недопустимым. В результате код пользователя больше не компилируется. Кроме того, теперь вообще запрещено вызывать этот метод, поскольку Java считает этот вызов недопустимым, так как вызывает супер-супер-метод косвенного суперкласса. Вместо этого вы можете вызвать метод по умолчанию
ComplexInput . Однако для этого потребуется сначала явно реализовать этот интерфейс в MyInput . Для пользователя библиотеки это изменение, скорее всего, довольно неожиданно!

Как ни странно, среда выполнения Java не делает этого различия. Верификатор JVM позволит скомпилированному классу вызывать SimpleInput::foo даже если загруженный класс во время выполнения неявно реализует ComplexClass , расширяя обновленную версию ThirdPartyBaseClass . Здесь жалуется только компилятор.

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