Статьи

Методы Java 8 по умолчанию: что можно, а что нельзя?

Какой метод по умолчанию

С выпуском Java 8 вы можете изменять интерфейсы, добавляя новые методы, чтобы интерфейс оставался совместимым с классами, которые реализуют интерфейс. Это очень важно, если вы разрабатываете библиотеку, которая будет использоваться несколькими программистами из Киева в Нью-Йорк. До рассвета Java 8, если вы опубликовали интерфейс в библиотеке, вы не могли добавить новый метод, не рискуя тем, что некоторые приложения, реализующие этот интерфейс, порвутся с новой версией интерфейса.

С Java 8 этот страх исчез? Нет.

Добавление метода по умолчанию к интерфейсу может сделать некоторый класс непригодным для использования.

Давайте сначала посмотрим на тонкости метода по умолчанию.

В Java 8 метод может быть реализован в интерфейсе. (Статические методы также могут быть реализованы в интерфейсе начиная с Java8, но это уже другая история.) Метод, реализованный в интерфейсе, называется методом по умолчанию и обозначается ключевым словом default как модификатор. Когда класс реализует интерфейс, он может, но не должен реализовывать метод, реализованный уже в интерфейсе. Класс наследует реализацию по умолчанию. Вот почему вам не нужно трогать класс, когда интерфейс, в котором он реализован, изменяется.

Множественное наследование?

Вещи начинают усложняться, когда конкретный класс реализует более одного (скажем, двух) интерфейсов, а интерфейсы реализуют один и тот же метод по умолчанию. Какой метод по умолчанию наследует класс? Ответ — нет. В таком случае класс должен реализовать сам метод (напрямую или по наследству от более высокого класса).

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

Однако вы не можете получить ошибку времени компиляции, если ваш класс уже скомпилирован. Таким образом, Java 8 не соответствует. У этого есть своя причина, которую я не хочу подробно описывать здесь или вступать в дебаты по разным причинам (например: релиз вышел, время дебатов давно истекло и никогда не было на этой платформе).

  • Допустим, у вас есть два интерфейса и класс, реализующий эти два интерфейса.
  • Один из интерфейсов реализует метод по умолчанию m() .
  • Вы компилируете все интерфейсы и класс.
  • Вы изменяете интерфейс, не содержащий метод m() чтобы объявить его абстрактным методом.
  • Скомпилируйте только измененный интерфейс.
  • Запустите класс.

множественное наследование
В этом случае класс работает. Вы не можете скомпилировать его снова с измененными интерфейсами, но если он был скомпилирован с более старой версией: он все еще работает. Теперь

  • измените интерфейс, имеющий абстрактный метод m() и создайте реализацию по умолчанию.
  • Скомпилируйте измененный интерфейс.
  • Запустите класс: провал.

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

множественные inheritance2
Класс совместим. Может быть загружен с новым интерфейсом. Он может даже запустить выполнение, если нет вызова метода, имеющего реализацию по умолчанию в обоих интерфейсах.

Образец кода

множественное наследование каталог

Чтобы продемонстрировать вышеизложенное, я создал тестовый каталог для класса C.java и три подкаталога для интерфейсов в файлах I1.java и I2.java . Корневой каталог теста содержит исходный код для класса C в файле C.java . База каталогов содержит версию интерфейса, которая подходит для выполнения и компиляции. I1 содержит метод m() с реализацией по умолчанию. Интерфейс I2 пока не содержит методов.

Класс содержит метод main, поэтому мы можем выполнить его в нашем тесте. Он проверяет, есть ли какой-либо аргумент командной строки, поэтому мы можем легко выполнить его с и без вызова метода m() .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
~/github/test$ cat C.java
public class C implements I1, I2 {
  public static void main(String[] args) {
    C c = new C();
    if( args.length == 0 ){
      c.m();
    }
  }
}
~/github/test$ cat base/I1.java
public interface I1 {
  default void m(){
    System.out.println("hello interface 1");
  }
}
~/github/test$ cat base/I2.java
public interface I2 {
}

Мы можем скомпилировать и запустить класс, используя командные строки:

1
2
3
~/github/test$ javac -cp .:base C.java
~/github/test$ java -cp .:base C
hello interface 1

compatible каталог содержит версию интерфейса I2 которая объявляет метод m() abstract, и по техническим причинам содержит I1.java без изменений.

1
2
3
4
5
~/github/test$ cat compatible/I2.java
 
public interface I2 {
  void m();
}

Это не может быть использовано для компиляции класса C :

1
2
3
4
5
~/github/test$ javac -cp .:compatible C.java
C.java:1: error: C is not abstract and does not override abstract method m() in I2
public class C implements I1, I2 {
       ^
1 error

Сообщение об ошибке очень точное. Несмотря на то, что у нас есть C.class из предыдущей компиляции, и если мы скомпилируем интерфейсы в compatible каталоге, у нас будет два интерфейса, которые все еще могут использоваться для запуска класса:

1
2
3
~/github/test$ javac compatible/I*.java
~/github/test$ java -cp .:compatible C
hello interface 1

Третий каталог wrong содержит версию I2 которая также определяет метод m() :

1
2
3
4
5
6
~/github/test$ cat wrong/I2.java
public interface I2 {
  default void m(){
    System.out.println("hello interface 2");
  }
}

Мы не должны даже потрудиться его скомпилировать. Несмотря на то, что метод определен дважды, класс все еще может быть выполнен до тех пор, пока он не вызывает метод, но происходит сбой, как только мы пытаемся вызвать метод m() . Вот для чего мы используем аргумент командной строки:

1
2
3
4
5
6
7
~/github/test$ javac wrong/*.java
~/github/test$ java -cp .:wrong C
Exception in thread "main" java.lang.IncompatibleClassChangeError: Conflicting default methods: I1.m I2.m
    at C.m(C.java)
    at C.main(C.java:5)
~/github/test$ java -cp .:wrong C x
~/github/test$

Вывод

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

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

Ссылка: Методы Java 8 по умолчанию: что можно, а что нельзя? от нашего партнера JCG Питера Верхаса из блога Java Deep .