Вопрос-ответ

In Java streams is peek really only for debugging?

В потоках Java peek действительно предназначен только для отладки?

Я читаю о Java streams и по ходу дела открываю для себя что-то новое. Одной из новых вещей, которые я нашел, была функция peek(). Почти все, что я читал на peek, говорит о том, что его следует использовать для отладки ваших потоков.

Что, если бы у меня был поток, в котором у каждой учетной записи есть имя пользователя, поле пароля и методы login() и LoggedIn ().

У меня также есть

Consumer<Account> login = account -> account.login();

и

Predicate<Account> loggedIn = account -> account.loggedIn();

Почему это было бы так плохо?

List<Account> accounts; //assume it's been setup
List<Account> loggedInAccount =
accounts.stream()
.peek(login)
.filter(loggedIn)
.collect(Collectors.toList());

Теперь, насколько я могу судить, это делает именно то, для чего предназначено. IT;


  • Принимает список учетных записей

  • Пытается войти в каждую учетную запись

  • Отфильтровывает любые учетные записи, которые не вошли в систему

  • Собирает зарегистрированные учетные записи в новый список

В чем недостаток выполнения чего-то подобного? Есть какие-либо причины, по которым я не должен продолжать? Наконец, если не это решение, то что?

В исходной версии этого приложения использовался метод .filter() следующим образом;

.filter(account -> {
account.login();
return account.loggedIn();
})
Переведено автоматически
Ответ 1

Важная вещь, которую вы должны понимать, это то, что потоки управляются терминальной операцией. Терминальная операция определяет, должны ли обрабатываться все элементы или какие-либо вообще. So collect - это операция, которая обрабатывает каждый элемент, тогда как findAny может прекратить обработку элементов, как только она встретит соответствующий элемент.

И count() может вообще не обрабатывать какие-либо элементы, когда он может определить размер потока без обработки элементов. Поскольку это оптимизация, не выполненная в Java 8, но которая будет в Java 9, могут возникнуть сюрпризы, когда вы переключитесь на Java 9 и будете использовать код, полагающийся на count() обработку всех элементов. Это также связано с другими деталями, зависящими от реализации, например, даже в Java 9 эталонная реализация не сможет предсказать размер источника бесконечного потока в сочетании с limit, хотя нет фундаментального ограничения, препятствующего такому прогнозированию.

Поскольку peek позволяет “выполнять указанное действие над каждым элементом по мере использования элементов из результирующего потока”, он не требует обработки элементов, но будет выполнять действие в зависимости от того, что требуется для работы терминала. Это означает, что вы должны использовать его с большой осторожностью, если вам нужна определенная обработка, например, хотите применить действие ко всем элементам. Это работает, если операция терминала гарантированно обрабатывает все элементы, но даже тогда вы должны быть уверены, что следующий разработчик не изменит операцию терминала (или вы забудете этот тонкий аспект).

Далее, в то время как streams гарантируют поддержание порядка встреч для определенной комбинации операций даже для параллельных потоков, эти гарантии не распространяются на peek. При сборе в список результирующий список будет иметь правильный порядок для упорядоченных параллельных потоков, но peek действие может вызываться в произвольном порядке и одновременно.

Итак, самое полезное, что вы можете сделать с помощью peek - это выяснить, был ли обработан элемент stream, что в точности соответствует тому, что указано в документации API:


Этот метод существует в основном для поддержки отладки, когда вы хотите видеть элементы по мере их прохождения мимо определенной точки в конвейере


Ответ 2

Ключевой вывод из этого:

Не используйте API непреднамеренным образом, даже если это достигает вашей непосредственной цели. Этот подход может дать сбой в будущем, и он также неясен будущим сопровождающим.


Нет ничего плохого в том, чтобы разбить это на несколько операций, поскольку это разные операции. В использовании API неясным и непреднамеренным образом есть вред, который может иметь последствия, если это конкретное поведение будет изменено в будущих версиях Java.

Использование forEach этой операции дало бы понять сопровождающему, что для каждого элемента существует предполагаемыйaccounts побочный эффект, и что вы выполняете некоторую операцию, которая может его изменить.

Это также более условно в том смысле, что peek это промежуточная операция, которая не выполняется со всей коллекцией до выполнения терминальной операции, но forEach это действительно терминальная операция. Таким образом, вы можете привести веские аргументы в отношении поведения и потока вашего кода, а не задавать вопросы о том, будет ли peek вести себя так же, как forEach в данном контексте.

accounts.forEach(a -> a.login());
List<Account> loggedInAccounts = accounts.stream()
.filter(Account::loggedIn)
.collect(Collectors.toList());
Ответ 3

Возможно, эмпирическим правилом должно быть то, что если вы используете peek вне сценария "debug", вам следует делать это только в том случае, если вы уверены в том, каковы условия завершения и промежуточной фильтрации. Например:

return list.stream().map(foo->foo.getBar())
.peek(bar->bar.publish("HELLO"))
.collect(Collectors.toList());

кажется, это допустимый случай, когда вы хотите за одну операцию преобразовать все Foo в Bars и передать им всем привет.

Кажется более эффективным и элегантным, чем что-то вроде:

List<Bar> bars = list.stream().map(foo->foo.getBar()).collect(Collectors.toList());
bars.forEach(bar->bar.publish("HELLO"));
return bars;

и в конечном итоге вам не придется повторять коллекцию дважды.

Ответ 4

Многие ответы содержали хорошие замечания, и особенно (принятый) ответ Макото довольно подробно описывает возможные проблемы. Но никто на самом деле не показал, как это может пойти не так.:

[1]-> IntStream.range(1, 10).peek(System.out::println).count();
| $6 ==> 9

Нет вывода.

[2]-> IntStream.range(1, 10).filter(i -> i%2==0).peek(System.out::println).count();
| $9 ==> 4

Выводит числа 2, 4, 6, 8.

[3]-> IntStream.range(1, 10).filter(i -> i > 0).peek(System.out::println).count();
| $12 ==> 9

Выводит числа от 1 до 9.

[4]-> IntStream.range(1, 10).map(i -> i * 2).peek(System.out::println).count();
| $16 ==> 9

Нет вывода.

[5]-> Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9).peek(System.out::println).count();
| $23 ==> 9

Нет вывода.

[6]-> Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9).stream().peek(System.out::println).count();
| $25 ==> 9

Нет вывода.

[7]-> IntStream.range(1, 10).filter(i -> true).peek(System.out::println).count();
| $30 ==> 9

Выводит числа от 1 до 9.

[1]-> List<Integer> list = new ArrayList<>();
| list ==> []
[2]-> Stream.of(1, 5, 2, 7, 3, 9, 8, 4, 6).sorted().peek(list::add).count();
| $7 ==> 9
[3]-> list
| list ==> []

(Вы поняли идею.)

Примеры были запущены в jshell (Java 15.0.2) и имитируют вариант преобразования данных (например, замените System.out::println на list::add, как это также делается в некоторых ответах) и возвращают, сколько данных было добавлено. Текущее наблюдение заключается в том, что любая операция, которая может фильтровать элементы (например, filter или skip), кажется, вызывает принудительную обработку всех оставшихся элементов, но это не обязательно должно оставаться таким образом.

java java-8 java-stream