У потоках 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); немає ніяких причин писати споживач і присудок, який просто викликає інший подібний метод.
Джошуа Тейлор

2
Також зауважте, що API потоку явно відмовляє від побічних ефектів у поведінкових параметрах .
Didier L

6
Корисні споживачі завжди мають побічні ефекти, але це, звичайно, не відлякує. Про це фактично йдеться в тому ж розділі: « Невелика кількість потокових операцій, таких як forEach()та peek(), може працювати лише за допомогою побічних ефектів; їх слід використовувати обережно. ”. Моє зауваження більше нагадувало, що peekоперацію (розроблену для цілей налагодження) не слід замінювати тим самим, що робиться всередині іншої операції, як map()або filter().
Didier L

Відповіді:


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());
Holger

1
@Holger: Відмінний момент. Я включив це у відповідь.
Макото

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:

Цей метод існує головним чином для підтримки налагодження, де потрібно бачити елементи, коли вони проходять повз певну точку трубопроводу


чи виникне якась проблема, майбутня чи теперішня, у випадку використання ОП? Чи завжди його код робить те, що він хоче?
ZhongYu

9
@ bayou.io: наскільки я бачу, в цій точній формі немає жодної проблеми . Але, як я намагався пояснити, використовуючи це таким чином, ви маєте пам’ятати про цей аспект, навіть якщо ви повернетесь до коду через один-два роки, щоб включити до коду «запит на функцію 9876»…
Holger

1
"дія підглядання може викликатись у довільному порядку та одночасно". Чи це твердження не суперечить їх правилу щодо того, як працює peek, наприклад, "як елементи споживаються"?
Хосе Мартінес

5
@Jose Martinez: У ньому йдеться про те, "як елементи споживаються з отриманого потоку ", це не термінальна дія, а обробка, хоча навіть термінальна дія може споживати елементи поза порядком, доки кінцевий результат є послідовним. Але я також думаю, що фраза примітки API " бачити елементи, коли вони проходять повз певну точку трубопроводу " робить кращу роботу при її описі.
Холгер

23

Можливо, головне правило повинно полягати в тому, що якщо ви заглядаєте за межами сценарію "налагодження", ви повинні робити це лише у тому випадку, якщо ви впевнені, які умови закінчення та проміжного фільтрування є. Наприклад:

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.