Почему я должен связывать операции Stream в Java?

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

Просто из любопытства я попробовал это:

import java.util.stream.IntStream;

class App {
    public static void main(String[] args) {
        IntStream is = IntStream.of(1, 2, 3, 4);
        is.map(i -> i + 1);
        int sum = is.sum();
    }
}

что в итоге выдает исключение времени выполнения:

Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
    at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:229)
    at java.util.stream.IntPipeline.reduce(IntPipeline.java:456)
    at java.util.stream.IntPipeline.sum(IntPipeline.java:414)
    at App.main(scratch.java:10)

Это обычно, я что-то упускаю, но все же хочу спросить: насколько я знаю, map - это промежуточная (и ленивая) операция, которая сама по себе ничего не делает в Stream. Только когда вызывается sum операции терминала (которая является энергичной операцией), поток используется и используется.

Но почему я должен их связывать?

В чем разница между

is.map(i -> i + 1);
is.sum();

а также

is.map(i -> i + 1).sum();

?

Ответ 1

Когда вы делаете это:

int sum = IntStream.of(1, 2, 3, 4).map(i -> i + 1).sum();

Каждый цепочечный метод вызывается на возвращаемом значении предыдущего метода в цепочке.

Таким образом, map вызывается для того, что IntStream.of(1, 2, 3, 4) и sum что возвращает map(i → я + 1).

Вам не нужно связывать потоковые методы, но они более читабельны и менее подвержены ошибкам, чем при использовании этого эквивалентного кода:

IntStream is = IntStream.of(1, 2, 3, 4);
is = is.map(i -> i + 1);
int sum = is.sum();

Что не совпадает с кодом, который вы указали в своем вопросе:

IntStream is = IntStream.of(1, 2, 3, 4);
is.map(i -> i + 1);
int sum = is.sum();

Как видите, вы игнорируете ссылку, возвращенную map. Это причина ошибки.


РЕДАКТИРОВАТЬ (согласно комментариям, спасибо @IanKemp за указание на это): На самом деле, это является внешней причиной ошибки. Если вы перестанете думать об этом, map должна что-то делать внутренне для самого потока, в противном случае, как тогда операция терминала вызовет преобразование, переданное в map для каждого элемента? Я согласен с тем, что промежуточные операции ленивы, то есть при вызове они ничего не делают с элементами потока. Но внутренне они должны сконфигурировать некоторое состояние в самом конвейере потока, чтобы их можно было применить позже.

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

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

  2. Он также устанавливает флаг для старого экземпляра потока, то есть того, к которому он был вызван, указывая, что этот экземпляр потока больше не представляет действительное состояние для конвейера. Это связано с тем, что новое обновленное состояние, содержащее функцию, переданную в map, теперь инкапсулируется возвращаемым экземпляром. (Я полагаю, что это решение могло быть принято командой jdk, чтобы ошибки появлялись как можно раньше, то есть, вызывая раннее исключение вместо того, чтобы позволить конвейеру перейти в недопустимое/старое состояние, которое не удерживает функцию для применять, тем самым позволяя работе терминала возвращать неожиданные результаты).

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


Другой способ увидеть все это - убедиться, что экземпляр Stream работает только один раз, посредством промежуточной или терминальной операции. Здесь вы нарушаете это требование, потому что вы вызываете map и sum в одном и том же экземпляре.

На самом деле, в Javadocs для Stream ясно сказано:

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

Ответ 2

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

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

Потоковая библиотека также хранит некоторое внутреннее отслеживание того, что происходит, поэтому она может генерировать исключение на этапе sum.

Если вы не хотите связывать, вы можете использовать переменную для каждого шага:

IntStream is = IntStream.of(1, 2, 3, 4);
IntStream is2 = is.map(i -> i + 1);
int sum = is2.sum();

Ответ 3

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

Взято из https://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.html в разделе "Операции с потоками и конвейеры"

На самом низком уровне все потоки управляются сплитератором.

Взято из той же ссылки в разделе "Низкоуровневое строительство потока".

Прохождение и расщепление выхлопных элементов; каждый Spliterator полезен только для одного массового вычисления.

Взято из https://docs.oracle.com/javase/8/docs/api/java/util/Spliterator.html