Java 8 Stream API - Гарантирует ли какая-либо промежуточная операция с сохранением состояния новую исходную коллекцию? - PullRequest
0 голосов
/ 11 сентября 2018

Верно ли следующее утверждение?

Операция sorted() является «промежуточной операцией с сохранением состояния», что означает, что последующие операции больше не работают с резервной коллекцией, но находятся во внутреннем состоянии.

( Источник и источник - они, похоже, копируются друг с другом или поступают из одного и того же источника.)

Я протестировал Stream::sorted как фрагмент из приведенных выше источников:

final List<Integer> list = IntStream.range(0, 10).boxed().collect(Collectors.toList());

list.stream()
    .filter(i -> i > 5)
    .sorted()
    .forEach(list::remove);

System.out.println(list);            // Prints [0, 1, 2, 3, 4, 5]

Это работает.Я заменил Stream::sorted на Stream::distinct, Stream::limit и Stream::skip:

final List<Integer> list = IntStream.range(0, 10).boxed().collect(Collectors.toList());

list.stream()
    .filter(i -> i > 5)
    .distinct()
    .forEach(list::remove);          // Throws NullPointerException

К моему удивлению, NullPointerException брошен.

Все протестированные методы соответствуют характеристикам с промежуточным состоянием .Тем не менее, это уникальное поведение Stream::sorted не задокументировано, а часть Потоковые операции и конвейеры объясняет, действительно ли промежуточные операции с состоянием действительно гарантируют новую коллекцию источников.

Откуда пришло мое замешательство и чем объясняется вышеупомянутое поведение?

Ответы [ 3 ]

0 голосов
/ 11 сентября 2018

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

Это не , а примерно sorted как таковое, но то, что может быть сделано для оптимизации потока, так что sorted может быть полностью пропущен. Например:

List<Integer> sortedList = IntStream.range(0, 10)
            .boxed()
            .collect(Collectors.toList());

    StreamSupport.stream(() -> sortedList.spliterator(), Spliterator.SORTED, false)
            .sorted()
            .forEach(sortedList::remove); // fails with CME, thus no copying occurred 

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

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

0 голосов
/ 11 сентября 2018

Вы не должны были поднимать дела с терминальной операцией forEach(list::remove), потому что list::remove является мешающей функцией и нарушает принцип "невмешательства" для действий терминала.

Очень важно следовать правилам, прежде чем задаться вопросом, почему неправильный фрагмент кода вызывает неожиданное (или недокументированное) поведение.

Я считаю, что list::remove является корнем проблемы здесь. Вы бы не заметили разницу между операциями для этого сценария, если бы написали правильное действие для forEach.

0 голосов
/ 11 сентября 2018

Документация по API не дает такой гарантии, что «последующие операции больше не работают с резервной коллекцией», следовательно, вы никогда не должны полагаться на такое поведение конкретной реализации.

В вашем примере происходитжелаемая вещь случайно;нет даже гарантии, что List, созданный collect(Collectors.toList()), поддерживает операцию remove.

Чтобы показать контрпример

Set<Integer> set = IntStream.range(0, 10).boxed()
    .collect(Collectors.toCollection(TreeSet::new));
set.stream()
    .filter(i -> i > 5)
    .sorted()
    .forEach(set::remove);

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

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

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

...