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
.