Ссылки на методы в Java

Лямбды  —  гибкие и анонимные фрагменты кода

Лямбды в 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 вместе с соответствующими им эквивалентами и типами функциональных интерфейсов, которые они принимают в качестве параметров.

Базовые методы RichIterable и With-методы

Как эти дополнительные методы помогают использовать ссылки на методы с параметрами? Рассмотрим на примере.

Базовое использование лямбд

Посмотрим на пример фильтрации списка строк с использованием одного из основных методов 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 значительно увеличивают общее количество ситуаций, в которых вы можете воспользоваться ссылками на методы вместо лямбд.

Читайте также:

Читайте нас в TelegramVK и Дзен


Перевод статьи Donald Raab: The elusive and beautiful Java Method Reference

Предыдущая статья5 причин грядущего господства Go в мире программирования
Следующая статьяКак отследить событие закрытия браузера и вкладки с помощью JavaScript