Cреди прочих нововведений в примере Hello,Streams был показан код, фильтрующий коллекцию:
1 | filter(s -> s.length() == WORD_LENGTH) |
Выражение, передающееся в filter() — лямбда-выражение, то есть блок кода с параметрами, который можно передать в другое место, поэтому он может быть выполнен позже, один или несколько раз.
Полный синтаксис лямбда выражений позволяет типизировать переменные, задавать им модификаторы или аннотации и использовать сложные конструкции в коде:
1 2 3 4 | (final String s) -> { System.out.println(s); return s.length() == WORD_LENGTH; } |
Синтаксис может быть упрощён: если в коде ровным счётом одно выражение, фигурные скобки вокруг кода могут быть опущены:
1 | (final String s) -> s.length() == WORD_LENGTH |
В случае единственного выражения, это выражение будет исполнено, а результат его исполнения автоматически возвращён из функции. Тип этого результата вычисляется автоматически.
В большинстве случаев тип параметров так же может быть определён автоматически и его можно пропустить:
1 | s -> s.length() == WORD_LENGTH |
Причём если параметр ровно один, можно опустить и скобки. Однако, для создания лямбда-выражения без параметров скобки необходимы:
1 | () -> { for (int i = 0; i < 1000; i++) System.out.println(i); } |
Функциональные интерфейсы
Лямбда-выражения может быть использовано там, где ожидается функциональный интерфейс, то есть интерфейс, реализующий единственный абстрактный метод. Тут надо отметить, что в Java8 появились интерфейсы, содержащие в себе код (и, следовательно, множественное наследование). Да и в ранних версиях языка можно было переопределять такие методы, как toString() прямо в интерфейсе.
1 2 3 4 | @FunctionalInterface public interface WordFilterInterface { boolean filter(String s); } |
Аннотация @FunctionalInterface говорит компилятору о ваших намерениях и позволяет проверить интерфейс на соответствие требованиям на этапе компиляции. Функциональный интерфейс можно использовать в качестве типа переменной, передаваемой в функцию:
1 2 3 4 5 6 7 8 9 10 | public final String greetVariable(WordFilterInterface filter) { StringBuilder result = new StringBuilder(); for (String s : LONG_WELCOME) { if (filter.filter(s)) { result.append(s); } } return result.toString(); } |
При это фактически параметром функции выступает некоторое лямбда-выражение, совместимое с единственным методом интерфейса по типу:
1 2 3 4 5 | @Test public void testGreetVariable() throws Exception { assertThat(testedObject.greetVariable(s -> s.length() == 4), is("java")); assertThat(testedObject.greetVariable(s -> s.length() == 6), isEmptyString()); } |
Такой подход к передаче лямбда-выражений c использованием функциональных интерфейсов немного напоминает реализацию интерфейсов в языке Go: там тоже отделены код, реализация и интерфейс и они ничего не знают друг о друге.
В Java8 имеется некоторое количество предопределённых функциональных интерфейсов, которые находятся в пакете java.util.function. Например, там определён интерфейс Predicate<T>, который ожидает метод filter(). Использование стандартных функциональных интерфейсов позволяет отказаться от доморощенного интерфейса, из примера выше, и тем самым упростить код и сделать его более читабельным:
1 2 3 4 5 6 | public final String greetPredicate(Predicate<String> filter) { return LONG_WELCOME .stream() .filter(filter) .collect(Collectors.joining(", ")); } |
1 2 3 4 5 | @Test public void testGreetPredicate() throws Exception { assertThat(testedObject.greetPredicate(s -> s.length() == 4), is("java")); assertThat(testedObject.greetPredicate(s -> s.length() == 6), isEmptyString()); } |
Обратите внимание, что хоть интерфейс и изменился, лямбда-выражение осталось тем же самым.
Доступ к переменным и замыкания
Лямбда-выражения имеют доступ к переменным области видимости, в которой их определили:
1 2 3 4 5 | @Test public void testGreetLocalVars() throws Exception { int l = 2; assertThat(testedObject.greetPredicate(s -> s.length() == l), is("to, of")); } |
Но доступ возможен только при условии, что переменные являются effective final, то есть либо явно имеют модификатор final, либо не меняют своего значения после инициализации.
С другой стороны, в java8 есть замыкания, то есть лямбда-выражение может использовать переменные уже после того, как действие их области видимости закончилось:
1 2 3 | public final Predicate<String> filterPredicate(Integer length) { return s -> s.length() == length; } |
1 2 3 4 5 | @Test public void testClosures() throws Exception { assertThat(testedObject.greetPredicate(testedObject.filterPredicate(4)), is("java")); assertThat(testedObject.greetPredicate(testedObject.filterPredicate(6)), isEmptyString()); } |
Очевидно, что к моменту использования лямбда-выражения локальная переменная length уже вышла из своей области видимости и превратилась в тыкву, но её значение осталось запомненным в конкретном экземпляре лямбда-выражения.
Ссылки на методы
Ссылки на методы позволяют отказаться даже от написания лямбда-выражений и просто указывать какой метод следует вызвать там, где ожидается лямбда-выражение. Синтаксис крайне прост:
1 2 3 4 5 | public Collection<String> sort() { List<String> sorted = new ArrayList<>(LONG_WELCOME); sorted.sort(String::compareToIgnoreCase); return sorted; } |
В данном коде String::compareToIgnoreCase эквивалентно лямбда-выражению (o,str) -> o.compareToIgnoreCase(str)
Аналогичным образом можно ссылаться на статические методы класса или на методы конкретного экземпляра класса:
1 2 3 4 | Class::staticMethod = x -> staticMethod(x) System.out::println = x -> System.out::println object::someMethod = x -> object.SomeMethod(x) |
В случае, когда ссылаются на не статический метод класса, подразумеваемый первый аргумент подразумеваемого лямбда выражения будет экземпляром этого класса, как показано в примере с сортировкой. Очевидно, что при использовании ссылок на методы вместо лямбда выражений требуется, чтобы методы были совместимыми по типу с ожидаемым интерфейсом и с учётом правил передачи переменных, описанных выше.
Так же можно ссылаться на методы объектов this или super, а так же на конструкторы Class::new. Последняя возможность широко используется при работе с потоковыми данными, которая будет описана в отдельной статье.
Код примера доступен на github.