Источник: Nuances of Programming
Лямбды — гибкие и анонимные фрагменты кода
Лямбды в Java полезны во многих направлениях. Лямбда-выражения можно использовать для более простых задач, а лямбда-утверждения — для более сложных. Лямбды могут вызывать другие методы для текущего объекта (this) и объектов, которые находятся в области видимости, таких как текущий элемент итерации и конечная локальная переменная за пределами лямбды. Лямбду всегда можно упростить, поместив код в другой метод.
Написание хороших лямбд требует дисциплины. Например, важно называть параметры понятным образом — так, чтобы названия раскрывали их назначение. Вот простой пример лямбды для фильтрации списка строк:
@Test
public void filterStringsLambda()
{
var list = Lists.mutable.with(
"Atlanta",
"Atlantic City",
"Boston",
"Boca Raton");
var actual = list.stream()
.filter(string -> string.startsWith("At"))
.collect(Collectors.toList());
var expected = List.of("Atlanta", "Atlantic City");
Assertions.assertEquals(expected, actual);
}
В этом коде лямбда — параметр, передаваемый методу filter в качестве предиката (Predicate). В данном примере предикат принимает параметр типа String, который назван string. Выражение после разделителя (->) будет вычисляться для каждого элемента списка и будет включать только те элементы, которые оцениваются как true.
В Stream API есть несколько методов, которые принимают предикат в качестве параметра. Например, filter, anyMatch, allMatch и noneMatch.
Здесь нет простого способа воспользоваться ссылкой на метод, потому что параметр “At” нужно передать методу startsWith. Параметры — криптонит для ссылок на методы. Мы можем симулировать нечто вроде ссылки на метод, написав лямбда-выражение и выделив его в отдельный метод следующим образом.
@Test
public void filterStringsLambdaInMethod()
{
var list = Lists.mutable.with(
"Atlanta",
"Atlantic City",
"Boston",
"Boca Raton");
var actual = list.stream()
.filter(this.stringStartsWith("At"))
.collect(Collectors.toList());
var expected = List.of("Atlanta", "Atlantic City");
Assertions.assertEquals(expected, actual);
}
private Predicate<String> stringStartsWith(String prefix)
{
return string -> string.startsWith(prefix);
}
Необходимость создавать метод в классе для генерации лямбд, которые могут использовать локальные переменные в области видимости, далека от идеала. Хотелось бы иметь более простую возможность применять метод startsWith в качестве ссылки на метод.
Как удовлетворить предпочтение ссылки на метод?
Задействовать методы With из Eclipse Collections.
Для многих методов, доступных в API Eclipse Collections, существует соответствующий дополнительный метод с суффиксом With. Каждый метод с With использует другой именованный функциональный интерфейс, который принимает два параметра (вторым будет, например, Predicate2, Function2 и т.д.). Следующая схема показывает некоторые из основных методов в API Eclipse Collections вместе с соответствующими им эквивалентами и типами функциональных интерфейсов, которые они принимают в качестве параметров.
Как эти дополнительные методы помогают использовать ссылки на методы с параметрами? Рассмотрим на примере.
Базовое использование лямбд
Посмотрим на пример фильтрации списка строк с использованием одного из основных методов Eclipse Collections с лямбдой.
@Test
public void selectStringsLambda()
{
var list = Lists.mutable.with(
"Atlanta",
"Atlantic City",
"Boston",
"Boca Raton");
var actual = list.select(string -> string.startsWith("At"));
var expected = List.of("Atlanta", "Atlantic City");
Assertions.assertEquals(expected, actual);
}
Ссылка на метод с With
Теперь посмотрим, как удовлетворить предпочтение для ссылки на метод, используя эквивалент метода select c “With”.
@Test
public void selectStringsWithMethodReference()
{
var list = Lists.mutable.with(
"Atlanta",
"Atlantic City",
"Boston",
"Boca Raton");
var actual = list.selectWith(String::startsWith, "At");
var expected = List.of("Atlanta", "Atlantic City");
Assertions.assertEquals(expected, actual);
}
Если у вас так и не случился момент озарения, не пугайтесь. Мы по-прежнему не можем передавать параметры ссылкам на методы напрямую. В настоящее время в Java нет синтаксиса, который это поддерживал бы. Здесь, видимо, какой-то подвох.
Попробую объяснить, как это работает. Метод selectWith принимает два параметра. Первый — Predicate2, который будет соответствовать сигнатуре String::StartsWith. А если точнее, Predicate2<String, String> соответствует сигнатуре String::startsWith. Второй параметр, selectWith, принимает параметр любого типа, который в данном случае является строкой.
Вот как в точности выглядит сигнатура selectWith для RichIterable.
<P> RichIterable<T> selectWith(
Predicate2<? super T, ? super P> predicate,
P parameter);
Пример реализации паттерна selectWith
В Eclipse Collection есть класс с именем IteratorIterate. Он включает многие базовые шаблоны итераций в Eclipse Collections, которые позволяют использовать шаблоны с любым итеративным типом Java. Я делюсь именно этим примером, потому что итератор — достаточно базовая концепция, и большинство разработчиков на Java смогут прочитать и понять такой код. Ниже показана реализация selectWith в IteratorIterate, которая сочетается со ссылками на методы с одним параметром.
public static <T, P, R extends Collection<T>> R selectWith(
Iterator<T> iterator,
Predicate2<? super T, ? super P> predicate,
P injectedValue,
R targetCollection)
{
while (iterator.hasNext())
{
T item = iterator.next();
if (predicate.accept(item, injectedValue))
{
targetCollection.add(item);
}
}
return targetCollection;
}
Этот паттерн может использоваться с любым типом, который способен создавать Iterator.
Вот пример использования IteratorIterate.selectWith с обычным Set из JDK.
@Test
public void selectWithOnIteratorIterate()
{
Set<String> strings = Set.of(
"Atlanta",
"Atlantic City",
"Boston",
"Boca Raton");
HashSet<String> actual = IteratorIterate.selectWith(
strings.iterator(),
String::startsWith,
"At",
new HashSet<>());
var expected = Set.of("Atlanta", "Atlantic City");
Assertions.assertEquals(expected, actual);
}
Больше ссылок на методы
Теперь, когда мы знаем, как использовать ссылку на метод с помощью With-метода, рассмотрим еще несколько примеров.
@Test
public void predicatesWithMethodReference()
{
var list = Lists.mutable.with(
"Atlanta",
"Atlantic City",
"Boston",
"Boca Raton");
var selected1 = list.selectWith(String::startsWith, "At");
var expected1 = List.of("Atlanta", "Atlantic City");
Assertions.assertEquals(expected1, selected1);
var rejected = list.rejectWith(String::startsWith, "At");
var expected2 = List.of("Boston", "Boca Raton");
Assertions.assertEquals(expected2, rejected);
var selected2 = list.selectWith(String::startsWith, "Bo");
Assertions.assertEquals(expected2, selected2);
var detected = list.detectWith(String::endsWith, "y");
Assertions.assertEquals("Atlantic City", detected);
var count = list.countWith(String::contains, "c");
Assertions.assertEquals(2, count);
Assertions.assertTrue(
list.anySatisfyWith(String::contains, "a"));
Assertions.assertTrue(
list.allSatisfyWith(String::contains, "t"));
Assertions.assertTrue(
list.noneSatisfyWith(String::contains, "z"));
var partitioned = list.partitionWith(String::endsWith, "n");
Assertions.assertEquals(expected2, partitioned.getSelected());
Assertions.assertEquals(expected1, partitioned.getRejected());
}
Существует множество методов, которые принимают единственный параметр, который может соответствовать Predicate2, Function2, Procedure2 и т.д., в качестве ссылок на методы. Методы With в Eclipse Collections значительно увеличивают общее количество ситуаций, в которых вы можете воспользоваться ссылками на методы вместо лямбд.
Читайте также:
Перевод статьи Donald Raab: The elusive and beautiful Java Method Reference