Лямбды — гибкие и анонимные фрагменты кода
Лямбды в 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 значительно увеличивают общее количество ситуаций, в которых вы можете воспользоваться ссылками на методы вместо лямбд.
Читайте также:
- Как написать на Java функцию, подобную sizeof в C
- Когда параллелизм превосходит конкурентность
- 10 языков программирования, которые пригодятся в 2023 году
Читайте нас в Telegram, VK и Дзен
Перевод статьи Donald Raab: The elusive and beautiful Java Method Reference