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

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

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

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

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

и

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

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

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

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

  • принимает список учетных записей
  • Пытается войти в каждую учетную запись
  • Фильтрует любую учетную запись, которая не войдет в систему
  • Собирает зарегистрированные учетные записи в новый список

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

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

.filter(account -> {
        account.login();
        return account.loggedIn();
    })

Ответ 1

Ключ от этого:

Не используйте 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());

Ответ 2

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

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

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

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

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

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

Ответ 3

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

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

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

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

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

и вы не заканчиваете повторять сборку дважды.

Ответ 4

Хотя я согласен с большинством ответов выше, у меня есть один случай, когда использование peek на самом деле кажется самым чистым способом.

Как и в случае использования, предположите, что вы хотите фильтровать только активные учетные записи, а затем выполнять вход в эти учетные записи.

accounts.stream()
    .filter(Account::isActive)
    .peek(login)
    .collect(Collectors.toList());

Peek помогает избежать избыточного вызова, не требуя повторной итерации коллекции дважды:

accounts.stream()
    .filter(Account::isActive)
    .map(account -> {
        account.login();
        return account;
    })
    .collect(Collectors.toList());

Ответ 5

Я бы сказал, что peek предоставляет возможность децентрализовать код, который может мутировать потоковые объекты или изменять глобальное состояние (на их основе), вместо того, чтобы набивать все в простую или сложенную функцию, переданную терминальному методу.

Теперь может возникнуть вопрос: должны ли мы мутировать потоковые объекты или изменять глобальное состояние из функций в функциональном java-программировании?

Если ответ на любой из вышеуказанных двух вопросов - да (или: в некоторых случаях да), то peek() определенно не только для целей отладки, по той же причине, что forEach() предназначен не только для целей отладки.

Для меня, когда вы выбираете между forEach() и peek(), выбираете следующее: хочу ли я фрагменты кода, которые мутируют объекты потока, которые должны быть прикреплены к составному, или я хочу, чтобы они напрямую привязывались к потоку?

Я думаю, peek() лучше будет сочетаться с java9 методами. например takeWhile() возможно, потребуется решить, когда остановить итерацию на основе уже мутировавшего объекта, поэтому его обработка с помощью forEach() не будет иметь такого же эффекта.

PS Я не ссылался на map() нигде, потому что если мы хотим мутировать объекты (или глобальное состояние), а не генерировать новые объекты, он работает точно так же, как peek().