В новой версии Java 8 наконец-то появились долгожданные лямбда-выражения. Возможно, это самая важная новая возможность последней версии; они позволяют писать быстрее и делают код более ясным, а также открывают дверь в мир функционального программирования. В этой статье я расскажу, как это работает.
Java задумывалась как объектно-ориентированный язык в 90-е годы, когда объектно-ориентированное программирование было главной парадигмой в разработке приложений. Задолго до этого было объектно-ориентированное программирование, были функциональные языки программирования, такие, как Lisp и Scheme, но их преимущества не были оценены за пределами академической среды. В последнее время функциональное программирование сильно выросло в значимости, потому что оно хорошо подходит для параллельного программирования и программирования, основанного на событиях («reactive»). Это не значит, что объектная ориентированность – плохо. Наоборот, вместо этого, выигрышная стратегия – смешивать объектно-ориентированное программирование и функциональное. Это имеет смысл, даже если вам не нужна параллельность. Например, библиотеки коллекций могут получить мощное API, если язык имеет удобный синтаксис для функциональных выражений.
Главным улучшением в Java 8 является добавление поддержки функциональных программных конструкций к его объектно-ориентированной основе. В этой статье я продемонстрирую основной синтаксис и как использовать его в нескольких важных контекстах. Ключевые моменты понятия лямбды:
- Лямбда-выражение является блоком кода с параметрами.
- Используйте лямбда-выражение, когда хотите выполнить блок кода в более поздний момент времени.
- Лямбда-выражения могут быть преобразованы в функциональные интерфейсы.
- Лямбда-выражения имеют доступ к final переменным из охватывающей области видимости.
- Ссылки на метод и конструктор ссылаются на методы или конструкторы без их вызова.
- Теперь вы можете добавить методы по умолчанию и статические методы к интерфейсам, которые обеспечивают конкретные реализации.
- Вы должны разрешать любые конфликты между методами по умолчанию из нескольких интерфейсов.
Зачем нужны лямбды?
Лямбда-выражение представляет собой блок кода, который можно передать в другое место, поэтому он может быть выполнен позже, один или несколько раз. Прежде чем углубляться в синтаксис (и любопытное название), давайте сделаем шаг назад и увидим, где вы использовали аналогичные блоки кода в Java до этого.
Если вы хотите выполнить действия в отдельном потоке, вы помещаете их в метод run
из Runnable
, вот так:
class MyRunner implements Runnable {
public void run() {
for (int i = 0; i < 1000; i++)
doWork();
}
...
}
Затем, когда вы хотите выполнить этот код, вы создаете экземпляр класса MyRunner
. Вы можете поместить экземпляр в пул потоков, или поступить проще и запустить новый поток:
MyRunner r = new MyRunner();
new Thread(r).start();
Ключевым моментом является то, что метод run
содержит код, который нужно выполнить в отдельном потоке.
Рассмотрим сортировку с использованием пользовательского компаратора. Если вы хотите отсортировать строки по длине, а не по умолчанию, вы можете передать объект Comparator
в метод sort
:
class LengthStringComparator implements Comparator<String> {
public int compare(String firstStr, String secondStr) {
return Integer.compare(firstStr.length(),secondStr.length());
}
}
Arrays.sort(strings, new LengthStringComparator ());
Метод sort
все так же вызывает метод compare
, переставляя элементы, если они стоят не по порядку, пока массив не будет отсортирован. Вы предоставляете методу sort
фрагмент кода, необходимый для сравнения элементов, и этот код встраивается в остальную часть логики сортировки, которую вам, вероятно, не нужно переопределять. Обратите внимание, что вызов Integer.compare (х, у)
возвращает ноль, если х и у равны, отрицательное число, если х < у, и положительное число, если х > у. Этот статический метод был добавлен в Java 7. Вы не должны вычислять х – y, чтобы сравнивать х и у, потому что расчет может вызвать переполнение для больших операндов противоположного знака.
В качестве другого примера отложенного выполнения, рассмотрим коллбэк для кнопки. Вы помещаете действие обратного вызова в метод класса, реализующего интерфейс слушателя, создаете экземпляр, и регистрируете экземпляр. Это настолько распространенный сценарий, что многие программисты используют синтаксис «анонимный экземпляр анонимного класса»:
button.setOnAction(new EventHandler<ActionEvent>() {
public void handle(ActionEvent event) {
System.out.println("The button has been clicked!");
}
});
Здесь важен код внутри метода handle
. Этот код выполняется всякий раз, когда нажимается кнопка.
Поскольку Java 8 позиционирует JavaFX в качестве преемника инструментария Swing GUI, я использую JavaFX в этих примерах. Детали не имеют значения. В каждой библиотеке пользовательского интерфейса, будь то Swing, JavaFX или Android, вы передаете кнопке некоторый код, который вы хотите запустить, когда кнопка нажата.
Во всех трех примерах вы видели один и тот же подход. Блок кода кому-то передавался — пулу потоков, методу сортировки или кнопке. Этот код вызывался некоторое время спустя.
До сих пор передача кода не была простой в Java. Вы не могли просто передать блоки кода куда угодно. Java является объектно-ориентированным языком, так что вы должны были создать объект, принадлежащий к классу, у которого есть метод с нужным кодом.
В других языках можно работать с блоками кода непосредственно. Проектировщики Java сопротивлялись добавлению этой функции в течение длительного времени. В конце концов, большая сила Java в ее простоте и последовательности. Язык может стать крайне беспорядочным, если будет включать в себя все функции, которые дают чуть более краткий код. Тем не менее, в тех других языках, это не просто легче порождать поток или зарегистрировать обработчик кнопки щелчка; многие их API проще, более последовательны и мощные. В Java, можно было бы написать подобные интерфейсы, которые принимают объекты классов, реализующих определенную функцию, но такие API было бы неудобно использовать.
В последнее время вопрос был не в том, расширять Java для функционального программирования или нет, а как это сделать. Потребовалось несколько лет экспериментов, прежде чем выяснилось, что это хорошо подходит для Java. В следующем разделе вы увидите, как можно работать с блоками кода в Java 8.
Синтаксис лямбда-выражений
Рассмотрим предыдущий пример сортировки еще раз. Мы передаем код, который проверяет, какая строка короче. Мы вычисляем
Integer.compare(firstStr.length(), secondStr.length())
Что такое firstStr
и secondStr
? Они оба строки! Java является строго типизированным языком, и мы должны указать типы:
(String firstStr, String secondStr)
-> Integer.compare(firstStr.length(),secondStr.length())
Вы только что видели ваше первое лямбда-выражение! Такое выражение является просто блоком кода вместе со спецификацией любых переменных, которые должны быть переданы в код.
Почему такое название? Много лет назад, когда еще не было никаких компьютеров, логик Алонзо Чёрч хотел формализовать, что значит для математической функции быть эффективно вычисляемой. (Любопытно, что есть функции, которые, как известно, существуют, но никто не знает, как вычислить их значения.) Он использовал греческую букву лямбда (λ), чтобы отметить параметры. Если бы он знал о Java API, он написал бы что-то не сильно похожее на то, что вы видели, скорее всего.
Почему буква λ? Разве Чёрч использовал все буквы алфавита? На самом деле, почтенный труд Principia Mathematica использует символ ˆ для обозначения свободных переменных, которые вдохновили Чёрча использовать заглавную лямбда (Λ) для параметров. Но, в конце концов, он переключился на строчной вариант буквы. С тех пор, выражение с переменными параметрами было названо «лямбда-выражение».
Вы только что видели одну форму лямбда-выражений в Java: параметры, стрелку -> и выражение. Если код выполняет вычисление, которое не вписывается в одно выражение, запишите его так же, как вы бы написали метод: заключенный в {} и с явными выражениями return
. Например,
(String firstStr, String secondStr) -> {
if (firstStr.length() < secondStr.length()) return -1;
else if (firstStr.length() > secondStr.length()) return 1;
else return 0;
}
Если лямбда-выражение не имеет параметров, вы все равно ставите пустые скобки, так же, как с методом без параметров:
() -> { for (int i = 0; i < 1000; i++) doWork(); }
Если типы параметров лямбда-выражения можно вывести, можно опустить их. Например,
Comparator<String> comp
= (firstStr, secondStr) // Same as (String firstStr, String secondStr)
-> Integer.compare(firstStr.length(),secondStr.length());
Здесь компилятор может сделать вывод, что firstStr
и secondStr
должны быть строками, потому что лямбда-выражение присваивается компаратору строк. (Мы посмотрим на это присваивание повнимательнее позже.)
Если метод имеет один параметр выводимого типа, вы можете даже опустить скобки:
EventHandler<ActionEvent> listener = event ->
System.out.println("The button has been clicked!");
// Instead of (event) -> or (ActionEvent event) ->
Вы можете добавить аннотации или модификатор final
к параметрам лямбды таким же образом, как и для параметров метода:
(final String var) -> ...
(@NonNull String var) -> ...
Вы никогда не указываете тип результата лямбда-выражения. Это всегда выясняется из контекста. Например, выражение
(String firstStr, String secondStr) -> Integer.compare(firstStr.length(), secondStr.length())
может быть использовано в контексте, где ожидается результат типа int
.
Обратите внимание, что лямбда-выражение не может возвращать значение в каких-то ветках, а в других не возвращать. Например, (int x) -> { if (x <= 1) return -1; }
является недопустимым.
Функциональные интерфейсы
Как мы уже обсуждали, в Java есть много существующих интерфейсов, которые инкапсулируют блоки кода, такие, как Runnable
или Comparator
. Лямбда-выражения имеют обратную совместимость с этими интерфейсами.
Вы можете поставить лямбда-выражение всякий раз, когда ожидается объект интерфейса с одним абстрактным методом. Такой интерфейс называется функциональным интерфейсом.
Вы можете удивиться, почему функциональный интерфейс должен иметь единственный абстрактный метод. Разве не все методы в интерфейсе абстрактные? На самом деле, всегда было возможно для интерфейса переопределить методы класса Object
, например, toString
или clone
, и эти объявления не делают методы абстрактными. (Некоторые интерфейсы в Java API переопределяют методы Object
, чтобы присоединить javadoc-комментарии. Посмотрите Comparator API для примера.) Что еще более важно, как вы вскоре увидите, в Java 8 интерфейсы могут объявлять неабстрактные методы.
Чтобы продемонстрировать преобразование в функциональный интерфейс, рассмотрим метод Arrays.sort
. Его второй параметр требуется экземпляр Comparator
, интерфейса с единственным методом. Просто предоставьте лямбду:
Arrays.sort(strs,
(firstStr, secondStr) -> Integer.compare(firstStr.length(), secondStr.length()));
За кулисами, метод Arrays.sort
получает объект некоторого класса, реализующего Comparator <String>
. Вызов метода compare
на этом объекте выполняет тело лямбда-выражения. Управление этими объектами и классами полностью зависит от реализации, и это может быть что-то гораздо более эффективное, чем использование традиционных внутренних классов. Лучше всего думать о лямбда-выражении как о функции, а не об объекте, и признать, что он может быть передан функциональному интерфейсу.
Это преобразование в интерфейсы – это то, что делает лямбда-выражения настолько мощными. Синтаксис короткий и простой. Вот еще один пример:
button.setOnAction(event ->
System.out.println("The button has been clicked!"));
Этот код очень легко читать.
В самом деле, преобразование в функциональный интерфейс — это единственное, что вы можете сделать с лямбда-выражением в Java. В других языках программирования, которые поддерживают функциональные литералы, можно объявить типы функций, таких как (String, String) -> int
, объявлять переменные этих типов, и использовать переменные для сохранения функциональных выражений. В Java вы не можете даже присвоить лямбда-выражение переменной типа Object
, потому Object
не является функциональным интерфейсом. Проектировщики Java решили строго придерживаться знакомой концепции интерфейсов, а не добавлять типы функций в язык.
Java API определяет несколько универсальных функциональных интерфейсов в пакете java.util.function. Один из интерфейсов, BiFunction <T, U, R>
, описывает функции с типами Т и U и типом возвращаемого значения R. Вы можете сохранить вашу лямбду сравнения строк в переменной этого типа:
BiFunction<String, String, Integer> compareFunc
= (firstStr, secondStr) -> Integer.compare(firstStr.length(), secondStr.length());
Тем не менее, это не поможет вам с сортировкой. Не существует метода Arrays.sort
, который принимает BiFunction
. Если вы использовали функциональный язык программирования и прежде, вы можете найти это любопытным. Но для Java программистов это довольно естественно. Такой интерфейс, как Comparator
, имеет конкретную цель, а не просто метод с заданным параметром и возвращаемым типом. Java 8 сохраняет этот стиль. Если вы хотите сделать что-то с лямбда-выражениями, вы все еще должны понимать назначение этого выражения, и иметь конкретный функциональный интерфейс для этого.
Интерфейсы из java.util.function используются в нескольких Java 8 интерфейсах API, и вы, вероятно, увидите их в других местах в будущем. Но имейте в виду, что вы можете одинаково хорошо преобразовать лямбда-выражение в функциональный интерфейс, который является частью любого API, который вы используете сегодня. Кроме того, вы можете пометить любой функциональный интерфейс с помощью аннотации @FunctionalInterface
. Это имеет два преимущества. Компилятор проверяет, что аннотированная сущность представляет собой интерфейс с одним абстрактным методом. И страница Javadoc включает в себя утверждение, что ваш интерфейс является функциональным интерфейсом. Вы не обязаны использовать аннотацию. Любой интерфейс с одним абстрактным методом является, по определению, функциональным интерфейсом. Но использование аннотации @FunctionalInterface
— это хорошая идея.
Наконец, заметим, что checked исключения могут возникнуть при преобразовании лямбды в экземпляр функционального интерфейса. Если тело лямбда-выражения может бросить checked исключение, это исключение должно быть объявлено в абстрактном методе целевого интерфейса. Например, следующее было бы ошибкой:
Runnable sleepingRunner = () -> { System.out.println("…"); Thread.sleep(1000); };
// Error: Thread.sleep can throw a checkedInterruptedException
Поскольку Runnable.run
не может бросить исключение, это присваивание является некорректным. Чтобы исправить ошибку, у вас есть два варианта. Вы можете поймать исключение в теле лямбда-выражения. Или вы можете присвоить лямбду интерфейсу, один абстрактный метод которого может бросить исключение. Например, метод call
из интерфейса Callable
может бросить любое исключение. Таким образом, вы можете присвоить лямбду Callable <void>
(если добавить return null
).
Ссылки на методы
Иногда уже есть метод, который осуществляет именно те действия, которые вы хотели бы передать в другое место. Например, предположим, что вы просто хотите распечатать объект события event
, когда кнопка нажата. Конечно, вы могли бы вызвать
button.setOnAction(event -> System.out.println(event));
Было бы лучше, если бы вы могли просто передать метод println
в метод setOnAction
. Примерно так:
button.setOnAction(System.out::println);
Выражение System.out::println
является ссылкой на метод, который эквивалентен лямбда-выражению x -> System.out.println(x)
.
В качестве другого примера, предположим, что вы хотите отсортировать строки независимо от регистра букв. Вы можете написать такой код:
Arrays.sort(strs, String::compareToIgnoreCase)
Как вы можете видеть из этих примеров оператор :: отделяет имя метода от имени объекта или класса. Есть три основных варианта:
- object::instanceMethod
- Class::staticMethod
- Class::instanceMethod
В первых двух случаях ссылка на метод эквивалентна лямбда-выражению, которое предоставляет параметры метода. Как уже упоминалось, System.out::println
эквивалентно x -> System.out.println(x)
. Точно так же, Math::pow
эквивалентно (x, y) -> Math.pow(x, y)
. В третьем случае первый параметр становится целевым объектом метода. Например, String::compareToIgnoreCase
— это то же самое, что и (x, y) -> x.compareToIgnoreCase(y)
.
При наличии нескольких перегруженных методов с тем же именем компилятор попытается найти из контекста, какой вы имеете в виду. Например, есть два варианта метода Math.max
, один для int
и один для double
. Какой из них будет вызван, зависит от параметров метода функционального интерфейса, к которому Math.max
преобразуется. Так же, как и лямбда-выражения, ссылки на методы не живут в изоляции. Они всегда преобразуются в экземпляры функциональных интерфейсов.
Вы можете захватить параметр this
в ссылке на метод. Например, this::equals
– это то же, что и x -> this.equals(x)
. Можно также использовать super
. Выражение super::instanceMethod
использует this
в качестве цели и вызывает версию данного метода суперкласса. Вот искусственный пример, который демонстрирует механизм:
class Speaker {
public void speak() {
System.out.println("Hello, world!");
}
}
class Concurrent Speaker extends Speaker {
public void speak() {
Thread t = new Thread(super::speak);
t.start();
}
}
При запуске потока вызывается его Runnable
, и super::speak
выполняется, вызывая speak
суперкласса. (Обратите внимание, что во внутреннем классе вы можете захватить эту ссылку из класса приложения, как EnclosingClass.this::method
или EnclosingClass.super::method
.)
Ссылки на конструктор
Ссылки на конструктор такие же, как ссылки на метод, за исключением того, что именем метода является new
. Например, Button::new
является ссылкой на конструктор класса Button
. На какой именно конструктор? Это зависит от контекста. Предположим, у вас есть список строк. Затем, вы можете превратить его в массив кнопок, путем вызова конструктора для каждой из строк, с помощью следующего вызова:
List<String> strs = ...;
Stream<Button> stream = strs.stream().map(Button::new);
List<Button> buttons = stream.collect(Collectors.toList());
Подробная информация о stream
, map
и методах collect
выходит за рамки этой статьи. На данный момент, важно то, что метод map
вызывает конструктор Button(String)
для каждого элемента списка. Есть несколько конструкторов Button
, но компилятор выбирает тот, что с параметром строкового типа, потому что из контекста ясно, что конструктор вызывается со строкой.
Вы можете сформировать ссылки на конструкторы с типом массивов. Например, int[]::new
является ссылкой на конструктор с одним параметром: длиной массива. Это равносильно лямбда-выражению x -> new int[x]
.
Ссылки на конструкторы массива полезны для преодоления ограничений Java. Невозможно создать массив универсального типа T. Выражение new T[n]
является ошибкой, так как оно будет заменено new Object[n]
. Это является проблемой для авторов библиотек. Например, предположим, мы хотим иметь массив кнопок. Интерфейс Stream
имеет метод toArray
, возвращающее массив Object
:
Object[] buttons = stream.toArray();
Но это неудовлетворительно. Пользователь хочет массив кнопок, а не объектов. Библиотека потоков решает эту проблему за счет ссылок на конструкторы. Передайте Button[]::new
методу toArray
:
Button[] buttons = stream.toArray(Button[]::new);
Метод toArray
вызывает этот конструктор для получения массива нужного типа. Затем он заполняет и возвращает массив.
Область действия переменной
Часто вы хотели бы иметь возможность получить доступ к переменным из охватывающего метода или класса в лямбда-выражении. Рассмотрим следующий пример:
public static void repeatText(String text, int count) {
Runnable r = () -> {
for (int i = 0; i < count; i++) {
System.out.println(text);
Thread.yield();
}
};
new Thread(r).start();
}
Рассмотрим вызов:
repeatText("Hi!", 2000); // Prints Hi 2000 times in a separate thread
Теперь посмотрим на переменные count
и text
внутри лямбда-выражения. Обратите внимание, что эти переменные не определены в лямбда-выражении. Вместо этого, они являются параметрами метода repeatText
.
Если подумать хорошенько, то не очевидно, что здесь происходит. Код лямбда-выражения может выполниться гораздо позже вызова repeatText
и переменные параметров уже будут потеряны. Как же переменные text
и count
остаются доступными?
Чтобы понять, что происходит, мы должны уточнить наши представления о лямбда-выражениях. Лямбда-выражение имеет три компонента:
- Блок кода
- Параметры
- Значения для свободных переменных; то есть, переменных, которые не являются параметрами и не определены в коде
В нашем примере лямбда-выражение имеет две свободных переменных, text
и count
. Структура данных, представляющая лямбда-выражение, должна сохранять значения для этих переменных, в нашем случае, «Hi!» и 2000. Будем говорить, что эти значения были захвачены лямбда-выражением. (Это деталь реализации, как это делается. Например, можно преобразовать лямбда-выражение в объект с одним методом, так что значения свободных переменных копируются в переменные экземпляра этого объекта.)
Техническим термином для блока кода вместе со значениями свободных переменных является замыкание. Если кто-то злорадствует, что их язык поддерживает замыкания, будьте уверены, что Java также их поддерживает. В Java лямбда-выражения являются замыканиями. На самом деле, внутренние классы были замыканиями все это время. Java 8 предоставляет нам замыкания с привлекательным синтаксисом.
Как вы видели, лямбда-выражение может захватить значение переменной в охватывающей области. В Java, чтобы убедиться, что захватили значение корректно, есть важное ограничение. В лямбда-выражении можно ссылаться только на переменные, значения которых не меняются. Например, следующий код является неправильным:
public static void repeatText(String text, int count) {
Runnable r = () -> {
while (count > 0) {
count--; // Error: Can't mutate captured variable
System.out.println(text);
Thread.yield();
}
};
new Thread(r).start();
}
Существует причина для этого ограничения. Изменяющиеся переменные в лямбда-выражениях не потокобезопасны. Рассмотрим последовательность параллельных задач, каждая из которых обновляет общий счетчик.
int matchCount = 0;
for (Path p : files)
new Thread(() -> { if (p has some property) matchCount++; }).start();
// Illegal to mutate matchCount
Если бы этот код был правомерным, это было бы не слишком хорошо. Приращение matchCount++
неатомарно, и нет никакого способа узнать, что произойдет, если несколько потоков выполнят этот код одновременно.
Внутренние классы могут также захватывать значения из охватывающей области. До Java 8 внутренние классы могли иметь доступ только к локальным final
переменным. Это правило теперь ослаблено для соответствия правилу для лямбда-выражений. Внутренний класс может получить доступ к любой эффективно final
локальной переменной; то есть, к любой переменной, значение которой не изменяется.
Не рассчитывайте, что компилятор выявит все параллельные ошибки доступа. Запрет на модификацию имеет место только для локальных переменных. Если matchCount
– переменная экземпляра или статическая переменная из охватывающего класса, то никакой ошибки не будет, хотя результат так же не определен.
Кроме того, совершенно законно изменять разделяемый объект, хоть это и не очень надежно. Например,
List<Path> matchedObjs = new ArrayList<>();
for (Path p : files)
new Thread(() -> { if (p has some property) matchedObjs.add(p); }).start();
// Legal to mutate matchedObjs, but unsafe
Обратите внимание, что переменная matchedObjs
эффективно final
. (Эффективно final
переменная является переменной, которой никогда не присваивается новое значение после ее инициализации.) В нашем случае matchedObjs
всегда ссылается на один и тот же объект ArrayList
. Однако объект изменяется, и это не потокобезопасно. Если несколько потоков вызовут метод add
, результат может быть непредсказуемым.
Существуют безопасные механизмы подсчета и сбора значений одновременно. Вы можете использовать потоки для сбора значений с определенными свойствами. В других ситуациях вы можете использовать потокобезопасные счетчики и коллекции.
Как и с внутренними классами, есть обходное решение, которое позволяет лямбда-выражению обновить счетчик в локальной охватывающей области видимости. Используйте массив длиной 1, вроде этого:
int[] counts = new int[1];
button.setOnAction(event -> counts[0]++);
Конечно, такой код не потокобезопасный. Для обратного вызова кнопки это не имеет значения, но в целом, вы должны подумать дважды, прежде чем использовать этот трюк.
Тело лямбда-выражения имеет ту же область видимости, что и вложенный блок. Здесь применяются те же самые правила для конфликтов имен. Нельзя объявить параметр или локальную переменную в лямбде, которые имеют то же имя, что и локальная переменная.
Path first = Paths.get("/usr/local");
Comparator<String> comp =
(first, second) -> Integer.compare(first.length(), second.length());
// Error: Variable first already defined
Внутри метода вы не можете иметь две локальные переменные с тем же именем. Таким образом, вы не можете объявить такие переменные также и в лямбда-выражении. При использовании ключевого слова this
в лямбда-выражении вы ссылаетесь на параметр this
метода, который создает лямбду. Рассмотрим, например, следующий код
public class Application() {
public void doWork() {
Runnable r = () -> { ...; System.out.println(this.toString()); ... };
...
}
}
Выражение this.toString()
вызывает метод toString
объекта Application
, а не экземпляра Runnable
. Нет ничего особенного в использовании this
в лямбда-выражении. Область видимости лямбда-выражения вложена внутрь метода doWork
, и this
имеет такое же значение в любой точке этого метода.
Методы по умолчанию
Многие языки программирования интегрируют функциональные выражения с их библиотеками коллекций. Это часто приводит к коду, который короче и проще для понимания, чем эквивалент, использующий циклы. Например, рассмотрим цикл:
for (int i = 0; i < strList.size(); i++)
System.out.println(strList.get(i));
Существует лучший способ. Авторы библиотеки могут предоставить метод forEach
, который применяет функцию к каждому элементу. Тогда вы можете просто вызвать
strList.forEach(System.out::println);
Это нормально, если библиотека коллекций была разработана с нуля. Но библиотека коллекций Java была разработана много лет назад, и есть проблема. Если интерфейс Collection
получает новые методы, такие как forEach
, то каждая программа, которая определяет свой собственный класс, реализующий Collection
, сломается, пока тоже не реализует этот метод. Это просто недопустимо в Java.
Проектировщики Java решили эту проблему раз и навсегда, позволив создавать методы интерфейса с конкретной реализацией (так называемые методы по умолчанию). Эти методы могут быть безопасно добавлены к существующим интерфейсам. В этом разделе мы рассмотрим методы по умолчанию в деталях. В Java 8 метод forEach
был добавлен к интерфейсу Iterable
, суперинтерфейсу Collection
, используя механизм, который я опишу здесь.
Рассмотрим такой интерфейс:
interface Person {
long getId();
default String getFirstName() { return "Jack"; }
}
Интерфейс имеет два метода: getId
, это абстрактный метод, и метод по умолчанию getFirstName
. Конкретный класс, реализующий интерфейс Person
, должен, конечно, предоставить реализацию getId
, но он может выбрать, оставить реализацию getFirstName
или переопределить ее.
Методы по умолчанию кладут конец классическому паттерну предоставления интерфейса и абстрактного класса, который реализует все или почти все из его методов, такие, как Collection/AbstractCollection
или WindowListener/WindowAdapter
. Теперь вы можете просто реализовать методы в интерфейсе.
Что произойдет, если точно такой же метод определен как метод по умолчанию в одном интерфейсе, а затем снова в качестве метода суперкласса или другого интерфейса? Языки типа Скала и C++ имеют сложные правила разрешения таких неоднозначностей. К счастью, правила в Java гораздо проще. Вот они:
- Родительские классы выиграют. Если суперкласс предоставляет конкретный метод, методы по умолчанию с тем же именем и типами параметров просто игнорируются.
- Интерфейсы сталкиваются. Если супер интерфейс предоставляет метод по умолчанию, а другой интерфейс поставляет метод с тем же именем и типами параметров (по умолчанию или нет), то вы должны разрешить конфликт путем переопределения этого метода.
Давайте посмотрим на второе правило. Рассмотрим еще один интерфейс с методом getFirstName
:
interface Naming {
default String getFirstName() { return getClass().getName() + "_" + hashCode(); }
}
Что произойдет, если вы создадите класс, реализующий оба?
class Student implements Person, Naming {
...
}
Класс наследует две противоречивые версии метода getFirstName
, предоставляемые интерфейсами Person
и Naming
. Вместо выбора того или другого метода, компилятор Java сообщает об ошибке и оставляет программисту разрешение неоднозначности. Просто предоставьте метод getFirstName
в классе Student
. В этом методе вы можете выбрать один из двух конфликтующих методов, как показано ниже:
class Student implements Person, Naming {
public String getFirstName() { returnPerson.super.getFirstName(); }
...
}
Теперь предположим, что Naming
интерфейс не содержит реализацию по умолчанию для getFirstName
:
interface Naming {
String getFirstName();
}
Может ли класс Student
унаследовать метод по умолчанию из интерфейса Person
? Это могло бы быть разумным, но проектировщики Java приняли решение в пользу единообразия. Это не имеет значения, как два интерфейсы конфликтуют. Если хотя бы один интерфейс обеспечивает реализацию, компилятор сообщает об ошибке, и программист должен устранить неоднозначность.
Если ни один интерфейс не обеспечивает реализацию по умолчанию для общего метода, то мы находимся в пре-Java 8 ситуации и нет никакого конфликта. У класса реализации есть две возможности: реализовать метод или оставить его нереализованным. В последнем случае класс сам является абстрактным.
Мы только что обсудили конфликты имен между двумя интерфейсами. Теперь рассмотрим класс, расширяющий суперкласс и реализующий интерфейс, наследуя тот же метод от обоих. Например, предположим, что Person
является классом и Student
определяется как:
class Student extends Person implements Naming { ... }
В этом случае только метод суперкласса имеет значение, и любой метод по умолчанию из интерфейса просто игнорируется. В нашем примере Student
наследует метод getFirstName
от Person
, и нет никакой разницы, обеспечивает ли интерфейс Named
реализацию по умолчанию для getFirstName
или нет. Это правило «класс побеждает» в действии. Правило «класс побеждает» обеспечивает совместимость с Java 7. Если вы добавляете методы по умолчанию к интерфейсу, это не имеет никакого влияния на код, который работал до того, как появились методы по умолчанию. Но имейте в виду: вы не имеете права создавать метод по умолчанию, который переопределяет один из методов класса Object
. Например, вы не можете определить метод по умолчанию для toString
или equals
, хотя это могло бы быть удобным для таких интерфейсов, как List
. Как следствие правила о победе классов, такой метод никогда не может выиграть у Object.toString
или Object.equals
.
Заключение
В этой статье были описаны лямбда-выражения в качестве единого крупнейшего обновления в модели программирования, когда-либо бывшего в Java, — большего, чем даже дженерики. А почему нет? Это открывает программисту Java возможности, которых ему не хватало по сравнению с другими функциональными языками программирования. Наряду с другими фичами, такими, как методы по умолчанию, лямбды могут быть использованы для написания действительно хорошего кода. Надеюсь, эта статья дала представление о то, что для нас приготовили в Java 8.
Автор: WildCat2013