Операции промежуточного потока не учитываются при подсчете


33

Кажется, у меня проблемы с пониманием того, как Java объединяет потоковые операции в потоковый конвейер.

При выполнении следующего кода

public
 static void main(String[] args) {
    StringBuilder sb = new StringBuilder();

    var count = Stream.of(new String[]{"1", "2", "3", "4"})
            .map(sb::append)
            .count();

    System.out.println(count);
    System.out.println(sb.toString());
}

Консоль только печатает 4. StringBuilderОбъект все еще имеет значение "".

Когда я добавляю операцию фильтра: filter(s -> true)

public static void main(String[] args) {
    StringBuilder sb = new StringBuilder();

    var count = Stream.of(new String[]{"1", "2", "3", "4"})
            .filter(s -> true)
            .map(sb::append)
            .count();

    System.out.println(count);
    System.out.println(sb.toString());
}

Выход изменяется на:

4
1234

Как эта, казалось бы, избыточная операция фильтра меняет поведение составного потокового конвейера?


2
Интересно !!!
uneq95

3
Я хотел бы представить, что это поведение, зависящее от реализации; возможно, это связано с тем, что первый поток имеет известный размер, а второй - нет, а размерность определяет, выполняются ли промежуточные операции.
Энди Тернер

Из интереса, что произойдет, если вы измените фильтр и карту?
Энди Тернер

Немного запрограммировавшись на Хаскеле, он пахнет как ленивая оценка, происходящая здесь. Поиск в Google вернулся, что потоки действительно лень. Может ли это быть так? А без фильтра, если Java достаточно умен, нет необходимости фактически выполнять сопоставление.
Frederik

@AndyTurner Это дает тот же результат, даже на развороте
uneq95

Ответы:


39

Операция count()терминала в моей версии JDK завершается выполнением следующего кода:

if (StreamOpFlag.SIZED.isKnown(helper.getStreamAndOpFlags()))
    return spliterator.getExactSizeIfKnown();
return super.evaluateSequential(helper, spliterator);

Если filter()в конвейере операций есть операция, размер потока, который известен изначально, больше не может быть известен (так как он filterможет отклонить некоторые элементы потока). Таким образом, ifблок не выполняется, выполняются промежуточные операции и, таким образом, изменяется StringBuilder.

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

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


Потому что flatMap()может быть в состоянии изменить количество элементов, было ли это причиной того, почему это было изначально нетерпеливым (теперь ленивым)? Таким образом, альтернативой было бы использовать forEach()и считать отдельно, если map()в его нынешнем виде нарушает договор, я полагаю.
Фредерик

3
Что касается flatMap, я так не думаю. Это было, AFAIK, потому что изначально было проще сделать это нетерпеливым. Да, использование потока с map () для создания побочных эффектов - плохая идея.
JB Низет

Не могли бы вы предложить, как добиться полного вывода 4 1234без использования дополнительного фильтра или создания побочных эффектов в операции map ()?
Аталант

1
int count = array.length; String result = String.join("", array);
JB Низет

1
или вы можете использовать forEach, если вы действительно хотите использовать StringBuilder, или вы можете использоватьCollectors.joining("")
njzk2

19

В jdk-9 это было четко задокументировано в документах java.

Избавление от побочных эффектов также может быть удивительным. За исключением операций терминала для forEach и forEachOrdered, побочные эффекты поведенческих параметров могут не всегда выполняться, когда реализация потока может оптимизировать выполнение поведенческих параметров, не влияя на результат вычисления. (Для конкретного примера см. Примечание API, документированное по операции подсчета .)

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

Реализация может предпочесть не выполнять конвейер потока (либо последовательно, либо параллельно), если она способна вычислить счет непосредственно из источника потока. В таких случаях исходные элементы не будут прослежены, а промежуточные операции не будут оцениваться. Могут быть затронуты поведенческие параметры с побочными эффектами, которые настоятельно не рекомендуется, за исключением безопасных случаев, таких как отладка. Например, рассмотрим следующий поток:

 List<String> l = Arrays.asList("A", "B", "C", "D");
 long count = l.stream().peek(System.out::println).count();

Число элементов, охватываемых источником потока, List, известно, и промежуточная операция peek не вводит и не удаляет элементы из потока (как это может быть в случае операций flatMap или фильтра). Таким образом, счетчик является размером списка, и нет необходимости выполнять конвейер и, как побочный эффект, распечатывать элементы списка.


0

Это не то, для чего .map. Предполагается, что он используется для превращения потока «Нечто» в поток «Нечто Остальное». В этом случае вы используете map для добавления строки во внешний Stringbuilder, после чего у вас есть поток «Stringbuilder», каждый из которых был создан операцией map, добавляющей один номер к исходному Stringbuilder.

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

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

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