Несколько недель назад я написал блог о том, что разработчики изучают новые языки, потому что они крутые. Я до сих пор придерживаюсь этого утверждения, потому что в Java 8 действительно круто. В то время как несомненной звездой шоу является добавление Lambdas и продвижение функций в переменные первого класса, мой текущий фаворит — методы по умолчанию. Это потому, что они являются таким аккуратным способом добавления новых функциональных возможностей в существующие интерфейсы без разрушения старого кода.
Реализация проста: взять интерфейс, добавить конкретный метод и прикрепить ключевое слово default
в качестве модификатора. Результатом является то, что внезапно все существующие реализации вашего интерфейса могут использовать этот код. В этом первом простом примере я добавил метод по умолчанию, который возвращает номер версии интерфейса 1 .
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
|
public interface Version { /** * Normal method - any old interface method: * * @return Return the implementing class's version */ public String version(); /** * Default method example. * * @return Return the version of this interface */ default String interfaceVersion() { return "1.0" ; } } |
Затем вы можете вызвать этот метод в любом классе реализации.
1
2
3
4
5
6
7
|
public class VersionImpl implements Version { @Override public String version() { return "My Version Impl" ; } } |
Вы можете спросить: почему это круто? Если вы возьмете интерфейс java.lang.Iterable и добавите следующий метод по умолчанию, вы получите конец цикла for
.
1
2
3
4
5
6
|
default void forEach(Consumer<? super T> action) { Objects.requireNonNull(action); for (T t : this ) { action.accept(t); } } |
Метод forEach
принимает в качестве аргумента экземпляр класса, который реализует интерфейс Consumer<T>
. Consumer<T>
можно найти в новом пакете java.util.function
и представляет собой то, что Java 8 называет функциональным интерфейсом , который представляет собой интерфейс, содержащий только один метод. В этом случае это метод accept(T t)
который принимает один аргумент и возвращает void
.
Пакет java.util.function
, вероятно, является одним из наиболее важных пакетов в Java 8. Он содержит целый набор отдельных методов или функциональных интерфейсов, которые описывают общие типы функций. Например, Consumer<T>
содержит функцию, которая принимает один аргумент и имеет возврат void
, в то время как Predicate<T>
является интерфейсом с функцией, которая принимает один аргумент и возвращает boolean
, которое обычно используется для записи фильтрующих лямбда-выражений.
Реализация этого интерфейса должна содержать все, что вы ранее записали в скобках for.
Так что, вы можете подумать, что это мне дает? Если это не Java 8, то ответ «не так много». Чтобы использовать метод forEach (…) до Java 8, вам нужно написать что-то вроде этого:
01
02
03
04
05
06
07
08
09
10
11
12
|
List<String> list = Arrays.asList( new String[] { "A" , "FirsT" , "DefaulT" , "LisT" }); System.out.println( "Java 6 version - anonymous class" ); Consumer<String> consumer = new Consumer<String>() { @Override public void accept(String t) { System.out.println(t); } }; list.forEach(consumer); |
Но если вы объедините это с лямбда-выражениями или ссылками на методы, вы получите возможность написать действительно классный код. Используя ссылку на метод, предыдущий пример становится:
1
|
list.forEach(System.out::println); |
Вы можете сделать то же самое с лямбда-выражением:
1
|
list.forEach((t) -> System.out.println(t)); |
Все это, похоже, соответствует одной из главных идей Java 8: пусть JDK сделает всю работу за вас. Перефразируя государственного деятеля и серийного филандера Джона Ф. Кеннеди, «не спрашивайте, что вы можете сделать со своим JDK, спрашивайте, что ваш JDK может сделать для вас» 2 .
Проблемы проектирования методов по умолчанию
Это новый классный способ написания вездесущего цикла for
, но есть ли проблемы с добавлением методов по умолчанию к интерфейсам, и если да, то каковы они и как их исправили ребята из проекта Java 8?
Первое, что нужно рассмотреть, это наследование. Что происходит, когда у вас есть интерфейс, который расширяет другой интерфейс и у обоих есть метод по умолчанию с одинаковой подписью? Например, что произойдет, если у вас есть SuperInterface
расширенный MiddleInterface
и MiddleInterface
расширенный SubInterface
?
1
2
3
4
5
6
|
public interface SuperInterface { default void printName() { System.out.println( "SUPERINTERFACE" ); } } |
1
2
3
4
5
6
7
|
public interface MiddleInterface extends SuperInterface { @Override default void printName() { System.out.println( "MIDDLEINTERFACE" ); } } |
1
2
3
4
5
6
7
|
public interface SubInterface extends MiddleInterface { @Override default void printName() { System.out.println( "SUBINTERFACE" ); } } |
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
public class Implementation implements SubInterface { public void anyOldMethod() { // Do something here } public static void main(String[] args) { SubInterface sub = new Implementation(); sub.printName(); MiddleInterface middle = new Implementation(); middle.printName(); SuperInterface sup = new Implementation(); sup.printName(); } } |
Независимо от того, каким способом вы его printName()
, printName()
всегда будет печатать «SUBINTERFACE».
Тот же вопрос возникает, когда у вас есть класс и интерфейс, содержащие одну и ту же сигнатуру метода: какой метод запускается? Ответ — правило «побед класса». Методы интерфейса по умолчанию всегда будут игнорироваться в пользу методов класса.
1
2
3
4
5
6
|
public interface AnyInterface { default String someMethod() { return "This is the interface" ; } } |
1
2
3
4
5
6
7
8
|
public class AnyClass implements AnyInterface { @Override public String someMethod() { return "This is the class - WINNING" ; } } |
Выполнение кода выше всегда выведет: «Это класс — ВЫИГРЫШ»
Наконец, что произойдет, если класс реализует два интерфейса и оба содержат методы с одинаковой сигнатурой? Это старая проблема C ++ diamond ; как вы решаете двусмысленность? Какой метод запускается?
1
2
3
4
5
6
|
public interface SuperInterface { default void printName() { System.out.println( "SUPERINTERFACE" ); } } |
1
2
3
4
5
6
|
public interface AnotherSuperInterface { default void printName() { System.out.println( "ANOTHERSUPERINTERFACE" ); } } |
В случае Java 8 ответ — ни один. Если вы попытаетесь реализовать оба интерфейса, вы получите следующую ошибку:
1
|
Duplicate default methods named printName with the parameters () and () are inherited from the types AnotherSuperInterface and SuperInterface. |
В случае, когда вы абсолютно ДОЛЖНЫ реализовать оба интерфейса, решение состоит в том, чтобы вызвать правило ‘class wins’ и переопределить неоднозначный метод в вашей реализации.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
public class Diamond implements SuperInterface, AnotherSuperInterface { /** Added to resolve ambiguity */ @Override public void printName() { System.out.println( "CLASS WINS" ); } public static void main(String[] args) { Diamond instance = new Diamond(); instance.printName(); } } |
Когда использовать методы по умолчанию
С точки зрения пуристов, добавление методов по умолчанию означает, что интерфейсы Java больше не являются интерфейсами. Интерфейсы были спроектированы как спецификация или контракт для предложенного / предполагаемого поведения: контракт, который ДОЛЖЕН выполнить класс реализации. Добавление методов по умолчанию означает, что между интерфейсами и абстрактными базовыми классами практически нет различий 3 . Это означает, что они открыты для злоупотреблений, поскольку некоторые неопытные разработчики могут подумать, что было бы здорово вырвать базовые классы из их кодовой базы и заменить их интерфейсами на основе методов по умолчанию — просто потому, что они могут, тогда как другие могут просто спутать абстрактные классы с интерфейсами, реализующими интерфейс по умолчанию. методы. В настоящее время я бы предложил использовать методы по умолчанию исключительно для их предполагаемого варианта использования: развитие устаревших интерфейсов без нарушения существующего кода. Хотя я могу передумать.
+1 Это не очень полезно, но это демонстрирует точку зрения…
2 Инаугурационная речь Джона Ф. Кеннеди 20 января 1961 года.
3 Абстрактные базовые классы могут иметь конструктор, а интерфейсы — нет. Классы могут иметь частные переменные экземпляра (т.е. состояние); интерфейсы не могут.
Ссылка: | Методы по умолчанию: Unsung Heros из Java 8 от нашего партнера по JCG Роджера Хьюза в блоге Captain Debug’s Blog . |