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.