Java 8 Stream API. Предоставляет ли какая-либо промежуточная промежуточная операция для обеспечения новой исходной коллекции?

Является ли следующее утверждение истинным?

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

Операция 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 не документировано, а часть операций Stream и конвейеров объясняет, действительно ли промежуточные операции с сохранением состояния гарантируют новую исходную коллекцию.

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

Ответы

Ответ 1

Документация 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() может быть преобразован в операцию "найти минимальную", без необходимости копировать элемент в новое хранилище для сортировки.

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

Ответ 2

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 с другой стороны, не обязательно должна быть полным барьером, все разные - проверяет один элемент за раз, если он уникален; поэтому после проверки одного элемента (и он уникален) он передается на следующий этап, таким образом, без полного барьера. В любом случае, это также не документировано...

Ответ 3

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

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

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