Идиомы Java 8 Каскадные лямбда-выражения

Java

Идиомы 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) и параметры из внешней области. То есть тело внутреннего лямбда-выражения зависит как от его параметров, так и от егоЛексическая областьилиОпределить область.

Каскадные лямбда-выражения имеют большой смысл для тех, кто их написал. Но как насчет читателей?

увидеть единственную стрелку вправо (->), вы должны понимать, что перед вами анонимная функция, которая принимает параметр (возможно, пустой) и выполняет действие или возвращает значение результата.

См. стрелку, содержащую две стрелки вправо (->) , вы также видите анонимную функцию, но она принимает параметр (который может быть пустым) и возвращает другую лямбду. Возвращаемое лямбда-выражение может принимать собственные параметры или может быть пустым. Он может выполнять действие или возвращать значение. Он может даже вернуть другое лямбда-выражение, но обычно это излишне, и его лучше избегать.

По сути, когда вы видите две стрелки вправо, вы можете думать обо всем, что находится справа от первой стрелки, как о черном ящике: лямбда-выражение, возвращаемое внешним лямбда-выражением.

заключительные замечания

Каскадные лямбда-выражения не очень распространены, но вы должны знать, как идентифицировать и понимать их в своем коде. Когда лямбда-выражение возвращает другое лямбда-выражение вместо выполнения действия или возврата значения, вы увидите две стрелки. Код такого типа очень короткий, но его может быть очень трудно понять при первом знакомстве. Однако, как только вы научитесь распознавать этот функциональный синтаксис, его станет намного легче понять и освоить.

связанная тема