Лямбда выражения в Java 8

В новой версии 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 { 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() { 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 comp = (firstStr, secondStr) // Same as (String firstStr, String secondStr) → Integer.compare (firstStr.length (), secondStr.length ()); Здесь компилятор может сделать вывод, что firstStr и secondStr должны быть строками, потому что лямбда-выражение присваивается компаратору строк. (Мы посмотрим на это присваивание повнимательнее позже.)Если метод имеет один параметр выводимого типа, вы можете даже опустить скобки:

EventHandler 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 . Вызов метода 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 , описывает функции с типами Т и U и типом возвращаемого значения R. Вы можете сохранить вашу лямбду сравнения строк в переменной этого типа:

BiFunction 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 (если добавить 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 strs = …; Stream