Это антипаттерн, чтобы использовать peek () для изменения элемента потока?


37

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

streamOfThings.peek(this::thingMutator).forEach(this::someConsumer);

Предположим, что изменение объектов в этой точке в коде является правильным поведением - например, thingMutatorметод может установить в поле «lastProcessed» текущее время.

Однако peek()в большинстве случаев означает «смотри, но не трогай».

Пользуемся peek()для мутировать элементы потока в антипаттернах или опрометчиво?

Редактировать:

Альтернативный, более традиционный подход заключается в том, чтобы преобразовать потребителя:

private void thingMutator(Thing thing) {
    thing.setLastProcessed(System.currentTimeMillis());
}

к функции, которая возвращает параметр:

private Thing thingMutator(Thing thing) {
    thing.setLastProcessed(currentTimeMillis());
    return thing;
}

и используйте map()вместо этого:

stream.map(this::thingMutator)...

Но это вводит небрежный код ( return), и я не уверен, что он понятнее, потому что, как вы знаете, peek()возвращает тот же объект, но с первого map()взгляда даже не ясно, что это тот же класс объекта.

Кроме того, у peek()вас может быть лямбда, которая мутирует, но с map()вами придется строить крушение поезда. Для сравнения:

stream.peek(t -> t.setLastProcessed(currentTimeMillis())).forEach(...)
stream.map(t -> {t.setLastProcessed(currentTimeMillis()); return t;}).forEach(...)

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

Что peek()касается меня , я не уклоняюсь от использования для изменения - я нахожу это очень удобным.



1
Вы использовали peekс потоком, который генерирует его элементы динамически? Это все еще работает, или изменения потеряны? Модификация элементов потока звучит для меня ненадежно.
Себастьян Редл

1
@SebastianRedl Я нашел это на 100% надежным. Я впервые использовал его для сбора во время обработки: List<Thing> list; things.stream().peek(list::add).forEach(...);очень удобно. Недавно. Я использовал его , чтобы добавить информацию для публикации: Map<Thing, Long> timestamps = ...; return things.stream().peek(t -> t.setTimestamp(timestamp.get(t))).collect(toList());. Я знаю, что есть другие способы сделать этот пример, но я сильно упрощаю здесь. Использование peek()дает более компактный и более элегантный код IMHO. Помимо читабельности, этот вопрос действительно о том, что вы подняли; это безопасно / надежно?
Богемский


@Bohemian. Вы все еще практикуете этот подход peek? У меня похожий вопрос по stackoverflow и надеюсь, что вы могли бы посмотреть на него и дать свой отзыв. Спасибо. stackoverflow.com/questions/47356992/…
tsolakp

Ответы:


23

Вы правы, «заглянуть» в английском смысле слова означает «смотри, но не трогай».

Однако в JavaDoc состояния:

заглядывать

Stream peek (Consumer action)

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

Ключевые слова: «совершать ... действие» и «потреблять». JavaDoc очень ясно, что мы должны peekиметь возможность изменять поток.

Однако JavaDoc также заявляет:

API Примечание:

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

Это указывает на то, что оно предназначено для наблюдения, например, для регистрации элементов в потоке.

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

По крайней мере, я бы добавил краткий комментарий к вашему коду:

// Note: this peek() call will modify the Things in the stream.
streamOfThings.peek(this::thingMutator).forEach(this::someConsumer);

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


1
Зачем вам нужен комментарий, если имя метода явно указывает на мутацию, например thingMutator, или более конкретную resetLastProcessedи т. Д. Если нет веских причин, комментарии о необходимости, такие как ваше предложение, обычно указывают на неправильный выбор имен переменных и / или методов. Если выбраны хорошие имена, разве этого недостаточно? Или вы говорите, что, несмотря на доброе имя, что-либо в нем peek()напоминает слепую зону, которую большинство программистов «скользит» (визуально пропускает)? Кроме того, «главным образом для отладки» - это не то же самое, что «только для отладки» - какие предназначения, кроме «в основном», были предназначены?
Богемский

@ Bohemian Я бы объяснил это главным образом потому, что имя метода не интуитивно понятно. И я не работаю на Oracle. Я не в их команде Java. Я понятия не имею, что они намеревались.

@ Bohemian, потому что при поиске того, что изменяет элементы так, как мне не нравится, я перестану читать строку в «peek», потому что «peek» ничего не меняет. Только позже, когда во всех других местах кода не будет ошибки, за которой я гонюсь, я вернусь к этому, возможно, с отладчиком, и, возможно, затем скажу: «О боже, кто поместил этот вводящий в заблуждение код ?!»
Фрэнк Хопкинс

@ Bohemian в экзамене здесь, где все в одной строке, нужно все равно читать, но можно пропустить содержимое «peek», и часто оператор потока переходит по нескольким строкам с одной операцией потока на строку
Фрэнк Хопкинс,

1

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


-2

Замечание по API говорит нам, что метод был добавлен в основном для таких действий, как отладка / регистрация / статистика запуска и т. Д.

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

       {@код
     * Stream.of («один», «два», «три», «четыре»)
     * .filter (e -> e.length ()> 3)
     * .peek (e -> System.out.println ("Отфильтрованное значение:" + e))
     * .map (String :: toUpperCase)
     * .peek (e -> System.out.println ("Отображенное значение:" + e))
     * .collect (Collectors.toList ());
     *}


2
Ваш ответ уже покрыт Snowman's.
Винсент

3
И «в основном» не означает «только».
Богемный

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