Статьи

Отложенное выполнение с потребителем Java

В одном из предыдущих постов в блоге (« Отложенное выполнение с поставщиком Java ») я ссылался на высказывание Кея Хорстманна в книге « Java SE8 для действительно нетерпеливых », касающееся лямбда-выражений: «Точка всех лямбд — это отложенное выполнение ». Хорстманн написал статью под названием « Лямбда-выражения в Java 8 » для журнала доктора Добба в последний год, в котором он написал аналогичное утверждение, используя другую терминологию: «Лямбда-выражение — это блок кода, который вы можете передать, чтобы его можно было выполняется позже, один раз или несколько раз ».

В той предыдущей статье я рассмотрел, как стандартный функциональный интерфейс Supplier используется с лямбда-выражениями в JDK для поддержки отложенного выполнения для случаев, когда одно значение «предоставляется только при необходимости» и без каких-либо аргументов, передаваемых ему. В этой статье я остановлюсь на предоставленных JDK примерах использования стандартного функционального интерфейса Consumer для «потребления» или «обработки» определенного блока кода «только при необходимости». В то время как Supplier принимает аргументов и возвращает ровно один ответ, Consumer принимает один или несколько аргументов и не возвращает ответа. Метод, вызванный для Supplier является методом get() метод accept(T) для Consumer . По определению ожидается, что Consumer будет иметь «побочные эффекты», поскольку он «потребляет» предоставленный кодовый блок.

В пакете java.util.function имеется множество стандартных функциональных интерфейсов в стиле Consumer . Ни один из них не возвращает результат (именно поэтому они являются потребителями!), Но они отличаются по количеству и типам аргументов, которые они принимают (но все они принимают по крайней мере один аргумент). Они перечислены здесь:

  • Потребитель — Генеральный Consumer который принимает один аргумент и будет в центре внимания большинства примеров этого поста.
  • BiConsumer — принимает два аргумента вместо одного («двухартеальная специализация потребителя »)
  • DoubleConsumer — Специализированный потребитель, предназначенный для примитивных double с
  • IntConsumer — Специализированный потребитель для примитивных int s
  • LongConsumer — специализированный потребитель, предназначенный для примитивных
  • ObjDoubleConsumer — Специализированный потребитель, который принимает два аргумента, первый из которых имеет тип Object а второй — double тип
  • ObjIntConsumer — специализированный потребитель, который принимает два аргумента, первый из которых имеет тип Object а второй — тип int
  • ObjLongConsumer — Специализированный потребитель, который принимает два аргумента, первый из которых имеет тип Object а второй — long

В оставшейся части этого поста мы рассмотрим подмножество использования JDK Consumer и связанных классов, чтобы продемонстрировать, как и когда они полезны.

Смотрит на поток элементов потока

В блоге « Просмотр внутренних потоков Java с помощью Stream.peek » я рассмотрел промежуточную операцию Stream.peek (Consumer), которую можно использовать для просмотра потоковых элементов потока. Это может быть очень полезно для понимания того, что различные операции потока делают с их соответствующими элементами потока. Распространенным способом сделать это является предоставление Consumer методу peek вызова System.out . println, который печатает текущий обработанный элемент потока в стандартный вывод (или регистрирует элемент или печатает его со стандартной ошибкой). Пример этого приведен в документации Javadoc для метода Stream.peek (Consumer):

1
2
3
4
5
6
Stream.of("one", "two", "three", "four")
   .filter(e -> e.length() > 3)
   .peek(e -> System.out.println("Filtered value: " + e))
   .map(String::toUpperCase)
   .peek(e -> System.out.println("Mapped value: " + e))
   .collect(Collectors.toList());

Поскольку различные перегруженные версии метода println(-) принимают параметр, но ничего не возвращают, они идеально соответствуют концепции «Потребитель».

Указание действия для элементов итерированного потока

В то время как Stream.peek(Consumer) является промежуточной операцией, Stream предоставляет два других метода, которые принимают Consumer которые являются обеими терминальными операциями и являются «для каждого» метода. Метод Stream.forEach (Consumer) — это метод, который выполняет действие, указанное предоставленным Consumer «явно недетерминированным» способом для элементов потока. Метод Stream.forEachOrdered (Consumer) выполняет действие, указанное предоставленным Consumer в « порядке встречи » потока, если этот поток имеет порядок встречи. В случаях обоих методов «действие», основанное на Consumer должно быть « не мешающим ». Оба метода продемонстрированы ниже.

01
02
03
04
05
06
07
08
09
10
11
12
13
Set.of("one", "two", "three", "four")
   .stream()
   .forEach(i -> out.println(i.toUpperCase()));
 
Stream.of("one", "two", "three", "four")
   .forEach(i -> out.println(i.toUpperCase()));
 
List.of("one", "two", "three", "four")
   .stream()
   .forEachOrdered(i -> out.println(i.toUpperCase()));
 
Stream.of("one", "two", "three", "four")
   .forEachOrdered(i -> out.println(i.toUpperCase()));

Приведенные выше примеры выглядят и очень похожи. Наиболее очевидная ситуация, в которой forEach может привести к резким отличиям от forEachOrdered — это использование параллельной обработки потока. В этом случае большинство отправляет использовать forEach вместо forEachOrdered .

Указание действия над повторяемыми элементами

В предыдущих примерах кода было показано использование Stream.forEach(Consumer) для итерации потока. Примеры также продемонстрировали, как сделать это для Set и List , сначала вызвав stream() для этих коллекций. Однако существуют удобные методы, которые определяются Iterable и реализуются этими реализациями коллекций, которые принимают Consumer и допускают итерацию этой коллекции с помощью метода forEach . Примеры этого показаны в следующем листинге кода.

1
2
3
4
Set.of("one", "two", "three", "four")
   .forEach(i -> out.println(i.toUpperCase()));
List.of("one", "two", "three", "four")
   .forEach(i -> out.println(i.toUpperCase()));

Хотя я и использовал коллекции в моем примере выше, все, что реализует Iterable , обычно будет поддерживать метод forEach (или нарушать объявленный контракт интерфейса).

Указание действия при итерации записей карты

Хотя интерфейс Java в Map не расширяет интерфейс Iterable как это делают Set и List , в Java Map все еще была предусмотрена аналогичная возможность указать потребителю «потреблять» каждую запись в Map . Поскольку у Map есть два входных аргумента (ключ и значение), ее метод forEach принимает BiConsumer вместо Consumer, который обсуждался в этом посте. Простой пример показан ниже.

1
2
3
4
5
Map.of("Denver", "Colorado",
       "Cheyenne", "Wyoming",
       "Salt Lake City", "Utah",
       "Boise", "Idaho")
   .forEach((c, s) -> out.println(c + " is the capital of " + s));

Хождение по стеку

StackWalker — это долгожданное дополнение к JDK 9, которое обеспечивает поточно- ориентированный подход к просмотру трассировки стека и является значительным улучшением по сравнению с подходом StackTraceElement . Возможно, разработчики чаще используют StackWalker.walk (Function) , но этот пост посвящен Consumer поэтому основное внимание уделяется StackWalker.forEach (Consumer) . Этот метод аналогичен ранее рассмотренным Stream.forEach и Iterable.forEach и продемонстрирован в следующем листинге кода.

1
StackWalker.getInstance().forEach(out::println);

Хотя в JDK используется гораздо больше Consumer , BiConsumer и других типов стандартных функциональных интерфейсов в стиле Consumer, последние примеры, о которых я расскажу в этом посте, относятся к классу Optional .

Применяется только при наличии

Методы Optional.ifPresent (Consumer) и Optional.ifPresentOrElse (Consumer) откладывают выполнение предоставленных Consumer , так что предоставленный Consumer будет вызываться, только если Optional не «пуст» (содержит null значение). Это простая, но мощная концепция, и упрощенные и надуманные примеры показывают, как они работают.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
public void demonstrateOptionalIfPresent()
{
   getMiddleName(true).ifPresent(n -> out.println("Middle Name: " + n));
}
 
public void demonstrateOptionalIfPresentOrElse()
{
   getMiddleName(false).ifPresentOrElse(
      n -> out.println("Middle Name: " + n),
      () -> displayMissingMiddleName());
}
 
private Optional<String> getMiddleName(final boolean present)
{
   return present ? Optional.of("Wayne") : Optional.empty();
}
 
private void displayMissingMiddleName()
{
   out.println("No middle name provided!");
}

Как показано в приведенном выше листинге кода, как Optional.ifPresent и Optional.ifPresentOrElse() представленный в JDK 9, вызывают предоставленного Consumer если Optional не пуст. Если Optional пуст, метод ifPresent ничего не делает, а ifPresentOrElse вызывает второй аргумент ( Runnable ).

Стандартные функциональные интерфейсы Java, которые принимают один или несколько аргументов и не возвращают результата, включают обычного Consumer а также некоторых специализированных потребителей. Они полезны для отсрочки выполнения до тех пор, пока не произойдет заданное условие (например, при повторении или при определении наличия), и поведение, которое нужно применить при возникновении этого условия, включает в себя один или несколько входных аргументов и нет необходимости предоставлять ответ. Примеры исходного кода, показанные в этом посте, доступны на GitHub .

Опубликовано на Java Code Geeks с разрешения Дастина Маркса, партнера нашей программы JCG . См. Оригинальную статью здесь: отложенное выполнение с потребителем Java

Мнения, высказанные участниками Java Code Geeks, являются их собственными.