Подтвердить что ты не робот

Java 8 findFirst и порядок встреч

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

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

Я попытался добавить элементы в Set, у которого нет определенного порядка встреч:

    Set<String> words = new HashSet<>();
    words.addAll(Arrays.asList("this", "is", "a", "stream", "of", "strings"));
    Optional<String> firstString = words.stream()
            .findFirst();
    System.out.println(firstString);

Каждый раз, когда я запускаю, я получаю a как первую строку. Затем я попытался сделать Collections.shuffle на List, прежде чем добавлять его в Set, но это ничего не изменило.

    List<String> wordList = Arrays.asList("this", "is", "a", "stream", "of", "strings");
    words = new HashSet<>();
    words.addAll(wordList);
    firstString = words.stream()
            .findFirst();
    System.out.println(firstString);

Я все равно возвращаю слово a каждый раз.

Затем я попытался использовать метод unordered из BaseStream, который утверждает, что возвращает поток без указания последовательности, но не имеет разницы:

    firstString = Stream.of("this", "is", "a", "stream", "of", "strings")
            .unordered()
            .findFirst();
    System.out.println(firstString);

Теперь я получаю слово this каждый раз. Я что-то упускаю? Есть ли способ продемонстрировать, что findFirst в неупорядоченном потоке возвращает разные значения?

4b9b3361

Ответ 1

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

Ваши лучшие шансы на показ разных результатов для findFirst связаны с параллельными потоками. Но даже там не каждая комбинация операций подходит для проявления неупорядоченности.

Один момент заключается в том, что в текущей реализации операция findFirst() не изменяет свое поведение, когда поток неупорядочен, т.е. он не пытается активно выглядеть как findAny(). Он может по-прежнему демонстрировать непредсказуемое поведение из-за источника Stream, но если ваш источник Stream.of("this", "is", "a", "stream", "of", "strings"), то есть неизменяемая последовательность известного размера, у него уже есть лучшая параллельная производительность, поэтому они просто не способ извлечь выгоду от unordered(), следовательно, текущая реализация не изменяет своего поведения.

Это может удивить, но это в некоторой степени относится к HashSet. Хотя у него есть неуказанный порядок, в какой-то момент времени будет существовать фактический порядок в его базовом массиве и до тех пор, пока вы не измените Set, не будет никакой причины перемешать эти элементы вокруг, поэтому для конкретного HashSet, вы можете неоднократно получать один и тот же "первый" элемент, хотя его не указано, какой из них и даже в пределах одной среды выполнения, другой экземпляр HashSet, представляющий одно и то же содержимое, но имеющий другую историю, может иметь другой порядок.


Одним из примеров операции, которая, как известно, извлекает выгоду из неупорядоченных характеристик, является distinct. Хотя он должен разбирать дубликаты, он должен поддерживать первое столкновение с равными элементами, если оно имеет заметную разницу. Это может значительно снизить производительность, поэтому реализация немедленно попытается получить выгоду, если поток неупорядочен. Например.

List<String> equal=IntStream.range(0, 100)
    .mapToObj(i->new String("test")) // don't do this in normal code
    .collect(Collectors.toList());
Map<String, Integer> map = IntStream.range(0, equal.size())
    .collect(IdentityHashMap::new, (m,i)->m.put(equal.get(i),i), Map::putAll);

equal.parallelStream().distinct().map(map::get)
     .findFirst().ifPresent(System.out::println);

Это создает кучу equal, но отличимых String экземпляров (которые вы обычно не должны делать), регистрирует их с их позиционным номером в IdentityHashMap, поэтому мы можем узнать, какой экземпляр distinct сохранен, Поскольку в приведенном выше коде используется упорядоченный поток, созданный с помощью List, он последовательно печатает 0, независимо от того, как часто вы его выполняете.

Напротив,

equal.parallelStream().unordered().distinct().map(map::get)
     .findFirst().ifPresent(System.out::println);

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


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

Ответ 2

Хольгер уже умело объяснил ситуацию. (+1) Я хотел бы предоставить демонстрацию экземпляров HashSet, которые имеют одинаковое содержимое, но имеют разные порядки итерации. Сначала мы создаем набор по-прежнему:

    List<String> wordList = Arrays.asList("this", "is", "a", "stream", "of", "strings");
    Set<String> words = new HashSet<>(wordList);

Мы создаем еще один набор слов, добавляем кучу материала (не имеет значения, что это такое), а затем удаляем его:

    Set<String> words2 = new HashSet<>(wordList);
    IntStream.range(0, 50).forEachOrdered(i -> words2.add(String.valueOf(i)));
    words2.retainAll(wordList);

Если мы проверим результаты следующим образом:

    System.out.println(words.equals(words2));
    System.out.println(words);
    System.out.println(words2);

мы видим на выходе, что наборы равны, но повторяются в другом порядке:

true
[a, strings, stream, of, this, is]
[this, is, strings, stream, of, a]

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

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

Хотя HashSets не имеет заданного порядка итерации, порядок, вероятно, будет повторяемым (и даже предсказуемым), если набор будет инициализирован одним и тем же содержимым одинаковым образом каждый раз. Таким образом, мы говорим, что поток из набора не имеет определенного порядка встреч, хотя порядок часто один и тот же каждый раз.

Обратите внимание, что в JDK 9 новые неизменяемые множества (и карты) на самом деле рандомизированы, поэтому их порядки итерации будут меняться от run to run, даже если они инициализируются одинаково каждый раз.

Ответ 3

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

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

 Set<String> words = new HashSet<>();
    words.addAll(Arrays.asList("this", "is", "a", "stream", "of", "strings"));
    Optional<String> firstString = words.stream().parallel()
            .findFirst();
    System.out.println(firstString);

Выполняя это несколько раз, показывает:

  Optional[strings] and then Optional[this]

Изменение набора в список и параллельная работа будет сохранять порядок:

 List<String> words = new ArrayList<>();
    words.addAll(Arrays.asList("this", "is", "a", "stream", "of", "strings"));
    Optional<String> firstString = words.stream().parallel()
            .findFirst();
    System.out.println(firstString); // always Optional[this]

Абсолютный должен прочитать здесь Хороший ответ Холгера

Ответ 4

Как уже упоминалось в @Eugene, вызов unordered не обязательно изменяет фактическое физическое упорядочение элементов. Не забывайте, что unordered - это промежуточная операция, которая ничего не делает до тех пор, пока не будет вызвана операция терминала.

Поэтому я склонен думать об этом так:

  • При создании Set, содержащего элементы "this", "is", "a", "stream", "of", "strings", тогда бывает, что первый элемент в Set при итерации по нему равен "a", поэтому findFirst просто возвращает это значение.

  • Когда вы создаете поток, используя Stream.of("this", "is", "stream", "of", "strings"), он возвращает поток с ограничением порядка, которое будет соблюдаться findFirst. Вызов unordered удаляет это ограничение, но элемент "this" по-прежнему физически является первым элементом, потому что unordered не обязательно меняет порядок в исходном массиве.

Более ярким примером может быть следующее:

Set<String> words = new HashSet<>();
words.addAll(Arrays.asList("this", "is", "stream", "of", "strings"));

Optional<String> firstString1 = words.stream().findFirst();
// Optional[strings]
System.out.println(firstString1);

Optional<String> firstString2 = words.stream()
                                     .sorted().findFirst();
// Optional[is]
System.out.println(firstString2);

Optional<String> firstString3 = Stream.of("this", "is", "stream", "of", "strings")
                                      .findFirst();
// Optional[this]
System.out.println(firstString3);

Optional<String> firstString4 = Stream.of("this", "is", "stream", "of", "strings")
                                      .unordered().findFirst();
// Optional[this]
System.out.println(firstString4);

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