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