Поведение Stream.skip с неупорядоченной работой терминала

Я уже читал этот и этот, но все же сомневаюсь, наблюдается ли наблюдаемое поведение Stream.skip был предназначен авторами JDK.

Пусть имеет простой ввод чисел 1..20:

List<Integer> input = IntStream.rangeClosed(1, 20).boxed().collect(Collectors.toList());

Теперь создайте параллельный поток, по-разному объедините unordered() с skip() и соберите результат:

System.out.println("skip-skip-unordered-toList: "
        + input.parallelStream().filter(x -> x > 0)
            .skip(1)
            .skip(1)
            .unordered()
            .collect(Collectors.toList()));
System.out.println("skip-unordered-skip-toList: "
        + input.parallelStream().filter(x -> x > 0)
            .skip(1)
            .unordered()
            .skip(1)
            .collect(Collectors.toList()));
System.out.println("unordered-skip-skip-toList: "
        + input.parallelStream().filter(x -> x > 0)
            .unordered()
            .skip(1)
            .skip(1)
            .collect(Collectors.toList()));

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

skip-skip-unordered-toList: [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
// absent values: 1, 2
skip-unordered-skip-toList: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 18, 19, 20]
// absent values: 1, 15
unordered-skip-skip-toList: [1, 2, 3, 4, 5, 6, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 19, 20]
// absent values: 7, 18

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

Позвольте пропустить один элемент и собрать в пользовательскую коллекцию в неупорядоченном режиме. Наша пользовательская коллекция будет HashSet:

System.out.println("skip-toCollection: "
        + input.parallelStream().filter(x -> x > 0)
        .skip(1)
        .unordered()
        .collect(Collectors.toCollection(HashSet::new)));

Выход удовлетворительный:

skip-toCollection: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
// 1 is skipped

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

Однако позвольте использовать эквивалентную неупорядоченную операцию терминала collect(Collectors.toSet()):

System.out.println("skip-toSet: "
        + input.parallelStream().filter(x -> x > 0)
            .skip(1)
            .unordered()
            .collect(Collectors.toSet()));

Теперь вывод:

skip-toSet: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 15, 16, 17, 18, 19, 20]
// 13 is skipped

Тот же результат может быть достигнут с любой другой неупорядоченной работой терминала (например, forEach, findAny, anyMatch и т.д.). Удаление шага unordered() в этом случае ничего не меняет. Кажется, что, хотя шаг unordered() правильно делает поток неупорядоченным, начиная с текущей операции, неупорядоченная операция терминала делает весь поток неупорядоченным, начиная с самого начала, несмотря на то, что это может повлиять на результат, если использовался skip(). Для меня это кажется совершенно неверным: я ожидаю, что использование неупорядоченного коллектора будет таким же, как превращение потока в неупорядоченный режим непосредственно перед операцией терминала и использование эквивалентного упорядоченного коллектора.

Итак, мои вопросы:

  • Предполагается ли это поведение или это ошибка?
  • Если да, то это где-то зарегистрировано? Я читал документацию Stream.skip(): он ничего не говорит о неупорядоченных терминальных операциях. Кроме того, Characteristics.UNORDERED документация не очень понятна и не говорит, что упорядочение будет потеряно для всего потока. Наконец, Ordering раздел в сводке пакетов также не распространяется на этот случай. Наверное, я что-то упустил?
  • Если он предполагал, что неупорядоченная операция терминала делает весь поток неупорядоченным, почему шаг unordered() делает его неупорядоченным только с этой точки? Могу ли я полагаться на это поведение? Или мне просто повезло, что мои первые тесты работают хорошо?

Ответ 1

Напомним, что целью флагов потока (ORDERED, SORTED, SIZED, DISTINCT) является включение операций, чтобы избежать ненужной работы. Примеры оптимизаций, которые включают флаги потока:

  • Если мы знаем, что поток уже отсортирован, то sorted() является no-op;
  • Если мы знаем размер потока, мы можем предварительно выделить массив правильного размера в toArray(), избегая копирования;
  • Если мы знаем, что вход не имеет значимого порядка встреч, нам не нужно предпринимать дополнительных шагов для сохранения порядка встреч.

Каждый этап конвейера имеет набор флагов потока. Промежуточные операции могут вводить, сохранять или очищать флаги потока. Например, фильтрация сохраняет сортировку/отчетность, но не размерность; отображение сохраняет размер, но не отсортировано или не определено. Сортировка сортирует инъекции. Обработка флагов для промежуточных операций довольно проста, поскольку все решения являются локальными.

Обработка флагов для терминальных операций более тонкая. ORDERED является самым важным флагом для терминальных операций. И если терминал op UNDERERED, мы возвращаем обратно неупорядоченность.

Зачем мы это делаем? Ну, рассмотрим этот трубопровод:

set.stream()
   .sorted()
   .forEach(System.out::println);

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

Предполагается ли это поведение или это ошибка?

Да:) Предполагается обратное распространение, так как это полезная оптимизация, которая не должна приводить к неправильным результатам. Тем не менее, частью ошибки является то, что мы распространяем прошлое предыдущее skip, чего мы не должны. Таким образом, обратное распространение флага UNORDERED является чрезмерно агрессивным, и это ошибка. Мы отправим сообщение об ошибке.

Если да, то это где-то документировано?

Это должна быть только деталь реализации; если бы он был правильно реализован, вы бы не заметили (за исключением того, что ваши потоки быстрее.)

Ответ 2

@Рубен, вы, вероятно, не понимаете моего вопроса. Примерно проблема is: why unordered(). collect (toCollection (HashSet:: new)) ведет себя иначе, чем собирать (toSet()). Конечно, я знаю, что toSet() неупорядоченный.

Наверное, но, во всяком случае, я дам ему вторую попытку.

Посмотрев на Javadocs коллекционеров toSet и toCollection, мы увидим, что toSet предоставляет неупорядоченный коллекционер

Это {@link Collector.Characteristics # UNORDERED неупорядоченный} Коллектор.

i.e., CollectorImpl с UNORDERED. Взглянув на Javadoc Collector. Характеристики # UNORDERED мы можем прочитать:

Указывает, что операция сбора не фиксирует сохранение порядок встреч входных элементов

В Javadocs of Collector мы также видим:

Для параллельных сборщиков реализация может свободно (но не требуется) одновременно осуществлять сокращение. Одновременное сокращение где функция аккумулятора называется одновременно несколько потоков, используя один и тот же одновременно изменяемый результат контейнер, а не сохранение результата, выделенного во время накопление. Одновременное сокращение следует применять только в том случае, если коллекционер имеет характеристики {@link Characteristics # UNORDERED} или если исходные данные неупорядочены

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

Btw, вы получите такое же поведение, если вы опустите неупорядоченный() в своем примере:

    System.out.println("skip-toSet: "
            + input.parallelStream().filter(x -> x > 0)
                .skip(1)
                .collect(Collectors.toSet()));

Кроме того, метод skip() в Stream дает нам подсказку:

В то время как {@code skip()} обычно является дешевой операцией на последовательном поточных трубопроводов, это может быть довольно дорогостоящим на упорядоченной параллели трубопроводы

и

Использование источника неупорядоченного потока (например, {@link #generate (Поставщик)}) или удаление ограничения упорядочения с помощью {@link #unordered()} может приводят к значительным ускорениям

При использовании

Collectors.toCollection(HashSet::new)

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