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


137

Я читаю о потоках Java и открываю для себя новые вещи. Одна из новых вещей, которую я нашел, была 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());

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

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

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

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

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

38
В любое время, когда мне нужна многострочная лямбда, я перемещаю строки в закрытый метод и передаю ссылку на метод вместо лямбды.
VGR

1
Какова цель - пытаетесь ли вы войти в систему всех учетных записей и отфильтровать их, если они вошли (что может быть тривиально верно)? Или вы хотите войти в них, а затем отфильтровать их в зависимости от того, вошли они или нет? Я спрашиваю об этом в этом порядке, потому что это forEachможет быть операция, которую вы хотите, а не peek. То, что он в API, не означает, что он не открыт для злоупотреблений (например Optional.of).
Макото

8
Также обратите внимание, что ваш код может быть просто .peek(Account::login)и .filter(Account::loggedIn); нет причин писать Consumer and Predicate, который просто вызывает другой метод, подобный этому.
Джошуа Тейлор

2
Также обратите внимание, что потоковый API явно препятствует побочным эффектам в поведенческих параметрах .
Дидье Л

6
У полезных потребителей всегда есть побочные эффекты, которые, конечно, не обескураживают. Это фактически упоминается в том же разделе: « Небольшое количество потоковых операций, таких как forEach()и peek(), может работать только через побочные эффекты; они должны использоваться с осторожностью. ». Мое замечание было больше напоминать, что peekоперацию (которая предназначена для целей отладки) не следует заменять, выполняя то же самое внутри другой операции, такой как map()или filter().
Дидье Л

Ответы:


77

Ключ к выводу из этого:

Не используйте 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
Если вы выполняете вход в систему на этапе предварительной обработки, вам вообще не нужен поток. Вы можете выступить forEachпрямо у источника коллекции:accounts.forEach(a -> a.login());
Хольгер

1
@ Хольгер: Отличная мысль. Я включил это в ответ.
Макото

2
@ Adam.J: Правильно, мой ответ был сосредоточен больше на общем вопросе, содержащемся в вашем заголовке, то есть действительно ли этот метод предназначен только для отладки, объясняя аспекты этого метода. Этот ответ более слит с вашим реальным вариантом использования и тем, как это сделать. Так что, можно сказать, вместе они дают полную картину. Во-первых, причина, по которой это не является предполагаемым использованием, во-вторых, вывод, чтобы не придерживаться непреднамеренного использования и что делать вместо этого. Последний будет более практичным для вас.
Хольгер

2
Конечно, было бы намного проще, если бы login()метод возвращал booleanзначение, указывающее статус успеха ...
Хольгер

3
Это то, к чему я стремился. Если login()возвращает a boolean, вы можете использовать его как предикат, который является самым чистым решением. У него все еще есть побочный эффект, но это нормально, если он не мешает, то есть loginпроцесс одного Accountне влияет на процесс входа другого Account.
Хольгер

111

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

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

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

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

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

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


будут ли какие-либо проблемы, будущие или настоящие, в случае использования OP? Его код всегда делает то, что хочет?
ZhongYu

9
@ bayou.io: насколько я вижу, в этом точном виде проблем нет . Но, как я пытался объяснить, использование этого способа подразумевает, что вы должны помнить об этом аспекте, даже если вы вернетесь к коду один или два года спустя, чтобы включить «запрос функции 9876» в код…
Хольгер

1
«Действие peek может быть вызвано в произвольном порядке и одновременно». Разве это утверждение не идет вразрез с их правилом работы взгляда, например, «как элементы потребляются»?
Хосе Мартинес

5
@Jose Martinez: в нем говорится «как элементы потребляются из результирующего потока », что является не действием терминала, а обработкой, хотя даже действие терминала может поглотить элементы не по порядку, если конечный результат непротиворечив. Но я также думаю, что фраза заметки API « видеть элементы по мере их прохождения через определенную точку в конвейере » лучше подходит для их описания.
Хольгер

23

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

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дает возможность децентрализовать код, который может изменять объекты потока или изменять глобальное состояние (на их основе), вместо того, чтобы вставлять все в простую или составную функцию, передаваемую терминальному методу.

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

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

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

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

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


3

Хотя я согласен с большинством ответов, приведенных выше, у меня есть один случай, когда использование 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());

3
Все, что вам нужно сделать, это правильно настроить метод входа. Я действительно не понимаю, как заглядывать - самый чистый путь. Как тот, кто читает ваш код, должен знать, что вы на самом деле неправильно используете API. Хороший и чистый код не заставляет читателя делать предположения о коде.
kaba713

1

Функциональное решение - сделать объект аккаунта неизменным. Таким образом, account.login () должен возвращать новый объект учетной записи. Это будет означать, что операция карты может быть использована для входа в систему вместо просмотра.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.