Идиомы Java 8
Каскадные лямбда-выражения
Повторно используемые функции помогают сделать код очень коротким, но может ли он быть слишком коротким?
Содержание серии:
Этот контент является частью серии:Идиомы Java 8
В функциональном программировании функции могут как получать, так и возвращать другие функции. Функции больше не такие, как в традиционном объектно-ориентированном программировании, а просто объекты.фабрикаилиСтроитель, который также может создавать и возвращать другую функцию. Функция, возвращающая функцию, может статькаскад лямбда-выражение, особенно стоит отметить, что код очень короткий. Хотя на первый взгляд этот синтаксис может показаться странным, он имеет свое применение. Эта статья поможет вам распознать каскадные лямбда-выражения, понять их природу и использование в коде.
загадочная грамматика
Вы видели такие фрагменты кода?
x -> y -> x > y
Если вам интересно «что, черт возьми, это значит?», вы не одиноки. Для разработчиков, незнакомых с программированием с помощью лямбда-выражений, этот синтаксис может выглядеть так, как будто товары падают из быстро движущегося грузовика.
К счастью, мы не видим их очень часто, но понимание того, как создавать каскадные лямбда-выражения и как понимать их в коде, значительно уменьшит ваше разочарование.
Функции высшего порядка
Прежде чем говорить о каскадных лямбда-выражениях, необходимо сначала понять, как их создавать. Для этого нам нужно просмотреть функции более высокого порядка (уже описанные в1-й в этой серии Статьявведены в), и онидекомпозиция функцииФункциональная декомпозиция — это способ разбить сложные процессы на более мелкие и простые части.
Сначала рассмотрим правила отличия функций высшего порядка от обычных функций:
обычная функция
- объект для получения
- объект может быть создан
- объект можно вернуть
Функции высшего порядка
- может получать функции
- можно создать функцию
- может вернуть функцию
Разработчики передают анонимные функции или лямбда-выражения функциям более высокого порядка, чтобы сделать код коротким и выразительным. Давайте рассмотрим два примера этих функций высшего порядка.
Пример 1: функция, которая получает функцию
В Java™ мы используем функциональные интерфейсы для ссылки на лямбда-выражения и ссылки на методы. Следующая функция принимает объект и функцию:
public static int totalSelectedValues(List<Integer> values,
Predicate<Integer> selector) {
return values.stream()
.filter(selector)
.reduce(0, Integer::sum);
}
totalSelectedValues
Первый параметр — это объект коллекции, а второй параметр —Predicate
функциональный интерфейс. потому что тип параметра является функциональным интерфейсом (Predicate
), так что теперь мы можем передать лямбда-выражение в качестве второго аргументаtotalSelectedValues
. Например, если мы хотимnumbers
в спискечетное значениеСумма, которую можно назватьtotalSelectedValues
,Следующее:
totalSelectedValues(numbers, e -> e % 2 == 0);
Предположим, мы сейчасUtil
Есть класс с именемisEven
изstatic
метод. В этом случае мы можем использоватьisEven
в видеtotalSelectedValues
параметр без передачи лямбда-выражения:
totalSelectedValues(numbers, Util::isEven);
Как правило, всякий раз, когда интерфейс функции появляется как параметр функциитип, то, что вы видите, является функцией более высокого порядка.
Пример 2: Функция, которая возвращает функцию
Функции могут получать функции, лямбда-выражения или ссылки на методы в качестве параметров. Точно так же функции могут возвращать лямбда-выражения или ссылки на методы. В этом случае возвращаемый тип будет функциональным интерфейсом.
Давайте сначала посмотрим на создание и возвратPredicate
Функция для проверки того, что заданное значение является нечетным:
public static Predicate<Integer> createIsOdd() {
Predicate<Integer> check = (Integer number) -> number % 2 != 0;
return check;
}
Чтобы вернуть функцию, мы должны предоставить функциональный интерфейс в качестве возвращаемого типа. В этом примере наш функциональный интерфейсPredicate
. Хотя приведенный выше код синтаксически верен, он мог бы быть намного короче. Мы улучшаем этот код, используя ссылки на типы и удаляя временные переменные:
public static Predicate<Integer> createIsOdd() {
return number -> number % 2 != 0;
}
Это используетсяcreateIsOdd
Пример метода:
Predicate<Integer> isOdd = createIsOdd();
isOdd.test(4);
Обратите внимание, что вisOdd
звонитьtest
вернусьfalse
. мы также можемisOdd
вызов с большим количеством значений наtest
; это не ограничивается одним использованием.
Создавайте повторно используемые функции
Теперь, когда у вас есть общее представление о функциях высшего порядка и о том, как их найти в коде, мы можем подумать об их использовании, чтобы сделать наш код короче.
Предположим, у нас есть два спискаnumbers1
иnumbers2
. Предположим, мы хотим извлечь из первого списка только числа больше 50, затем из второго списка извлечь значения больше 50 иумножить на 2.
Это может быть достигнуто с помощью следующего кода:
List<Integer> result1 = numbers1.stream()
.filter(e -> e > 50)
.collect(toList());
List<Integer> result2 = numbers2.stream()
.filter(e -> e > 50)
.map(e -> e * 2)
.collect(toList());
Этот код хорош, но вы заметили, что он многословен? Мы используем лямбда-выражение, которое дважды проверяет, больше ли число 50. Мы можем создавать и повторно использоватьPredicate
, который удаляет повторяющийся код и делает его более выразительным:
Predicate<Integer> isGreaterThan50 = number -> number > 50;
List<Integer> result1 = numbers1.stream()
.filter(isGreaterThan50)
.collect(toList());
List<Integer> result2 = numbers2.stream()
.filter(isGreaterThan50)
.map(e -> e * 2)
.collect(toList());
Сохраняя лямбда-выражение в ссылке, мы можем использовать его повторно, что позволяет избежать дублирования лямбда-выражений. Если мы хотим повторно использовать лямбда-выражение в разных методах, мы также можем поместить ссылку в отдельный метод вместо ссылки на локальную переменную.
Теперь предположим, что мы хотим начать со спискаnumbers1
Извлеките значения больше 25, 50 и 75. Мы можем начать с написания 3 разных лямбда-выражений:
List<Integer> valuesOver25 = numbers1.stream()
.filter(e -> e > 25)
.collect(toList());
List<Integer> valuesOver50 = numbers1.stream()
.filter(e -> e > 50)
.collect(toList());
List<Integer> valuesOver75 = numbers1.stream()
.filter(e -> e > 75)
.collect(toList());
Хотя каждое из приведенных выше лямбда-выражений сравнивает ввод с другим значением, они делают одно и то же. Как я могу переписать этот код с меньшим количеством повторений?
Создание и повторное использование лямбда-выражений
Хотя два лямбда-выражения в предыдущем примере одинаковы, три приведенных выше выражения немного отличаются. создать возвратPredicate
изFunction
может решить эту проблему.
Во-первых, функциональный интерфейсFunction<T, U>
положитьT
ввод типа преобразуется вU
тип выхода. Например, в следующем примере заданное значение преобразуется в его квадратный корень:
Function<Integer, Double> sqrt = value -> Math.sqrt(value);
Здесь возвращаемый типU
может быть таким же простым, какDouble
,String
илиPerson
. Или это также может быть более сложным, напримерConsumer
илиPredicate
и так далее для другого функционального интерфейса.
В этом примере мы хотимFunction
СоздаватьPredicate
. Итак, код выглядит следующим образом:
Function<Integer, Predicate<Integer>> isGreaterThan = (Integer pivot) -> {
Predicate<Integer> isGreaterThanPivot = (Integer candidate) -> {
return candidate > pivot;
};
return isGreaterThanPivot;
};
ЦитироватьisGreaterThan
Цитировать представительствоFunction<T, U>
— или, точнее, означаетFunction<Integer, Predicate<Integer>>
лямбда-выражение. вход - этоInteger
, выходPredicate<Integer>
.
в теле лямбда-выражения (внешний{}
внутри), мы создаем еще одну ссылкуisGreaterThanPivot
, который содержит ссылку на другое лямбда-выражение. На этот раз ссылкаPredicate
вместоFunction
. Наконец, мы возвращаем эту ссылку.
isGreaterThan
является ссылкой на лямбда-выражение, которое возвращается при вызовеДругаялямбда-выражения — другими словами, здесь скрыта каскадная связь лямбда-выражений.
Теперь мы можем использовать только что созданное внешнее лямба-выражение для устранения дублирования в коде:
List<Integer> valuesOver25 = numbers1.stream()
.filter(isGreaterThan.apply(25))
.collect(toList());
List<Integer> valuesOver50 = numbers1.stream()
.filter(isGreaterThan.apply(50))
.collect(toList());
List<Integer> valuesOver75 = numbers1.stream()
.filter(isGreaterThan.apply(75))
.collect(toList());
существуетisGreaterThan
звонитьapply
вернетPredicate
, который затем передается в качестве аргументаfilter
метод.
Хотя весь процесс очень прост (в качестве примера), возможность абстрагироваться в функцию особенно полезна для сценариев, где предикаты более сложны.
Советы по краткости
Мы успешно удалили повторяющиеся лямбда-выражения из нашего кода, ноisGreaterThan
Определение по-прежнему выглядит беспорядочно. К счастью, мы можем комбинировать некоторые соглашения Java 8, чтобы уменьшить беспорядок и сделать код короче.
Начнем с рефакторинга следующего кода:
Function<Integer, Predicate<Integer>> isGreaterThan = (Integer pivot) -> {
Predicate<Integer> isGreaterThanPivot = (Integer candidate) -> {
return candidate > pivot;
};
return isGreaterThanPivot;
};
Ссылки на типы можно использовать для удаления сведений о типах из параметров внешних и внутренних лямбда-выражений:
Function<Integer, Predicate<Integer>> isGreaterThan = (pivot) -> {
Predicate<Integer> isGreaterThanPivot = (candidate) -> {
return candidate > pivot;
};
return isGreaterThanPivot;
};
В настоящее время мы удалили два слова из кода с небольшим улучшением.
Далее удаляем лишнее()
и ненужная временная ссылка во внешнем лямбда-выражении:
Function<Integer, Predicate<Integer>> isGreaterThan = pivot -> {
return candidate -> {
return candidate > pivot;
};
};
Код короче, но все равно выглядит немного беспорядочно.
Вы можете видеть, что тело внутреннего лямбда-выражения, очевидно, состоит только из одной строки.{}
иreturn
является избыточным. Удалим их:
Function<Integer, Predicate<Integer>> isGreaterThan = pivot -> {
return candidate -> candidate > pivot;
};
Теперь вы можете видеть, что тело внешнего лямбда-выражениятакжетолько одна строка, поэтому{}
иreturn
Здесь тоже лишнее. Здесь мы применяем последний рефакторинг:
Function<Integer, Predicate<Integer>> isGreaterThan =
pivot -> candidate -> candidate > pivot;
Теперь вы можете видеть — это наше каскадное лямбда-выражение.
Понимание каскадных лямбда-выражений
Мы проходим процесс рефакторинга, который подходит каждому этапу, и добираемся до финального кода — каскадных лямбда-выражений. В этом случае внешнее лямбда-выражение получаетpivot
В качестве параметра внутреннее лямбда-выражение получаетcandidate
как параметр. Тело внутреннего лямбда-выражения также использует полученные аргументы (candidate
) и параметры из внешней области. То есть тело внутреннего лямбда-выражения зависит как от его параметров, так и от егоЛексическая областьилиОпределить область.
Каскадные лямбда-выражения имеют большой смысл для тех, кто их написал. Но как насчет читателей?
увидеть единственную стрелку вправо (->
), вы должны понимать, что перед вами анонимная функция, которая принимает параметр (возможно, пустой) и выполняет действие или возвращает значение результата.
См. стрелку, содержащую две стрелки вправо (->
) , вы также видите анонимную функцию, но она принимает параметр (который может быть пустым) и возвращает другую лямбду. Возвращаемое лямбда-выражение может принимать собственные параметры или может быть пустым. Он может выполнять действие или возвращать значение. Он может даже вернуть другое лямбда-выражение, но обычно это излишне, и его лучше избегать.
По сути, когда вы видите две стрелки вправо, вы можете думать обо всем, что находится справа от первой стрелки, как о черном ящике: лямбда-выражение, возвращаемое внешним лямбда-выражением.
заключительные замечания
Каскадные лямбда-выражения не очень распространены, но вы должны знать, как идентифицировать и понимать их в своем коде. Когда лямбда-выражение возвращает другое лямбда-выражение вместо выполнения действия или возврата значения, вы увидите две стрелки. Код такого типа очень короткий, но его может быть очень трудно понять при первом знакомстве. Однако, как только вы научитесь распознавать этот функциональный синтаксис, его станет намного легче понять и освоить.