JavaRush /Java блог /Архив info.javarush /Методы по умолчанию в Java 8: что могут и чего не могут?
Spitfire
33 уровень

Методы по умолчанию в Java 8: что могут и чего не могут?

Статья из группы Архив info.javarush
Перевод статьи, написанной Peter Verhas от апреля 2014 года. Методы по умолчанию в Java 8: что могут и чего не могут? - 1От переводчика: термин "default method" в Java только появился и я не уверен, есть ли для него устоявшийся перевод на русский. Я буду использовать термин "метод по умолчанию", хотя и не считаю его идеальным. Приглашаю к обсуждению относительно более удачного перевода.

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

Теперь, с выходом Java 8, можно добавлять в интерфейсы новые методы так, что этот интерфейс остается совместимым с классами, которые его реализуют. Это очень важно, если вы разрабатываете библиотеку, которую используют множество программистов от Киева до Нью-Йорка. До выхода Java 8, если вы описали интерфейс в библиотеке, вы не могли добавлять в него методы не рискуя, что какое-нибудь приложение, работающее с вашим интерфейсом, не сломается при его обновлении. Так что, в Java 8 этого можно больше не бояться? Нет, нельзя. Добавление метода по умолчанию в интерфейс может привести к невозможности использования некоторых классов. Давайте сначала посмотрим на хорошие стороны методов по умолчанию. В Java 8 метод можно реализовать прямо в интерфейсе. (Статические методы в интерфейсе теперь тоже можно реализовывать, но это другая история.) Метод, реализованный в интерфейсе, называется методом по умолчанию и обозначается ключевым словом default. Если класс реализует интерфейс, он может, но не обязан, реализовать методы, реализованные в интерфейсе. Класс наследует реализацию по умолчанию. Вот почему не обязательно модифицировать классы при изменении интерфейса, который они реализуют.

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

Все усложняется, если некий класс реализует более одного (скажем, два) интерфейса, а они реализуют один и тот же самый метод по умолчанию. Какой из методов унаследует класс? Ответ — никакой. В таком случае класс должен реализовать метод самостоятельно (напрямую, либо унаследовав его от другого класса). Ситуация аналогична, если только один интерфейс имеет метод по умолчанию, а в другом этот же метод является абстрактным. Java 8 старается быть дисциплинированной и избегать неоднозначных ситуаций. Если методы объявлены более чем в одном интерфейсе, то никакой реализации по умолчанию классом не наследуется - вы получите ошибку компиляции. Хотя, ошибку компиляции можно и не получить, если ваш класс уже скомпилирован. В этом отношении Java 8 недостаточно стойка. На то есть свои причины, в обсуждение которых я не хочу вдаваться (например: релиз Java уже вышел и время для дискуссий уже давно прошло и вообще, здесь для них не место).
  • Скажем, у вас есть два интерфейса, и класс реализует их оба.
  • Один из интерфейсов реализует метод по умолчанию m().
  • Вы компилируете все интерфейсы и класс.
  • Вы меняете интерфейс, в котором нет метода m(), объявляя его как абстрактный метод.
  • Компилируете только модифицированный интерфейс.
  • Запускаете класс.
Методы по умолчанию в Java 8: что могут и чего не могут? - 2В этом случае класс работает. Вы не можете его скомпилировать с обновленными интерфейсами, но он был скомпилирован со старыми версиями и потому работает. Теперь
  • измените интерфейс с абстрактным методом m() и добавьте реализацию по умолчанию.
  • Скомпилируйте модифицированный интерфейс.
  • Запустите класс: ошибка.
Когда есть два интерфейса, предоставляющих реализацию метода по умолчанию, этот метод не может быть вызван в классе, если он не реализован самим классом (опять таки, самостоятельно, либо унаследовано от другого класса). Методы по умолчанию в Java 8: что могут и чего не могут? - 3Класс совместим. Он может быть загружен с модифицированным интерфейсом. Он может даже работать до тех пор, пока не будет вызван метод, имеющий реализацию по умолчанию в обоих интерфейсах.

Пример кода

Методы по умолчанию в Java 8: что могут и чего не могут? - 4Для того, что бы продемонстрировать вышесказанное, я создал тестовый каталог для класс C.java и 3 подкаталога для интерфейсов в файлах I1.java и I2.java. Корневой каталог для теста содержит исходный код класса C.java. Каталог base содержит версию интерфейсов, которые подходят для выполнения и компиляции: интерфейс I1 имеет метод по умолчанию m(); интерфейс I2 пока не имеет никаких методов. В классе есть метод main, так что мы можем выполнить его для проверки. Он проверяет, если ли какие-то аргументы командной строки, так что мы легко можем выполнить его как с вызовом, так и без вызова метода m().

~/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 {
}
Можно скомпилировать и выполнить класс из командной строки.

~/github/test$ javac -cp .:base C.java
~/github/test$ java -cp .:base C
hello interface 1
Каталог compatible содержит версию интерфейса I2, который объявляет метод m() абстрактным, а также, по техническим причинам, не измененную копию I1.java.

~/github/test$ cat compatible/I2.java
 
public interface I2 {
  void m();
}
Такой набор нельзя использовать для компиляции класса C:

~/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, у нас будет два интерфейса , которые все еще можно использовать для запуска класса:

~/github/test$ javac compatible/I*.java
~/github/test$ java -cp .:compatible C
hello interface 1
Третий каталог — wrong — содержит версию I2, в котором также объявлен метод m():

~/github/test$ cat wrong/I2.java
public interface I2 {
  default void m(){
    System.out.println("hello interface 2");
  }
}
Можно даже не мучиться с компиляцией. Несмотря на то, что метод объявлен дважды, класс все еще может использоваться и работать, пока не произойдет вызов метода m(). Вот для чего нам нужен аргумент командной строки:

~/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 с использованием этой фичи.
Комментарии (4)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Vitaly Khant Уровень 0
27 декабря 2018
Вероятность такой коллизии крайне мала, так что можно не беспокоиться
Taras Kutselya Уровень 24
8 сентября 2018
Немного сложно...
MSBlast Уровень 30
29 апреля 2014
В таком случае класс должен реализовать класс самостоятельно
видимо подразумевается «реализовать интерфейс»?

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

В целом познавательная статья. Спасибо за труды.