Почему Java 8 stream forEach цикл дублирует результаты после использования метода sorted()

#java #foreach #java-stream

#java #foreach #java-stream

Вопрос:

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

Когда я выполняю сортировку перед filter() и forEach() методами

         words.stream()
                .sorted()
                .filter(s1 -> !alreadyFound.contains(s1) amp;amp; words.stream()
                        .filter(s2 -> isAnagram.apply(s1, s2))
                        .count() == maxAnagrams)
                .forEach(s1 -> {
  

Это дает эти результаты:

  abel able bale bela elba
 alger glare lager large regal
 angel angle galen glean lange
 caret carte cater crate trace
 elan lane lean lena neal
 evil levi live veil vile
  

Но когда я использую методы sorted() after filter() и before forEach()

         words.stream()
                .filter(s1 -> !alreadyFound.contains(s1) amp;amp; words.stream()
                        .filter(s2 -> isAnagram.apply(s1, s2))
                        .count() == maxAnagrams)
                .sorted()
  

Затем он выдает эти результаты:

  abel able bale bela elba
 abel able bale bela elba
 alger glare lager large regal
 angel angle galen glean lange
 angel angle galen glean lange
 abel able bale bela elba
 abel able bale bela elba
 caret carte cater crate trace
 caret carte cater crate trace
 caret carte cater crate trace
 caret carte cater crate trace
 elan lane lean lena neal
 abel able bale bela elba
 evil levi live veil vile
 angel angle galen glean lange
 alger glare lager large regal
 angel angle galen glean lange
 alger glare lager large regal
 elan lane lean lena neal
 angel angle galen glean lange
 alger glare lager large regal
 elan lane lean lena neal
 elan lane lean lena neal
 evil levi live veil vile
 evil levi live veil vile
 elan lane lean lena neal
 alger glare lager large regal
 caret carte cater crate trace
 evil levi live veil vile
 evil levi live veil vile
  

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

Я использую:

 jdk1.8.0_201
  

Пример кода:

 import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.function.BiFunction;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class Main2 {
    public static void main(String[] args) {
        List<String> words = new ArrayList<>();
        List<String> alreadyFound = new ArrayList<>();

        BiFunction<String, String, Boolean> isAnagram = (s1, s2) -> {
            if (s1.length() != s2.length()) return false;
            char[] c1 = s1.toCharArray();
            char[] c2 = s2.toCharArray();
            Arrays.sort(c1);
            Arrays.sort(c2);
            return Arrays.equals(c1, c2);
        };

        try (InputStream inputStream = new URL("http://wiki.puzzlers.org/pub/wordlists/unixdict.txt").openStream();
             InputStreamReader inputStreamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
             Stream<String> stream = new BufferedReader(inputStreamReader).lines()) {
            stream.forEach(words::add);
        } catch (IOException e1) {
            e1.printStackTrace();
        }
        long maxAnagrams = Collections.max(words.stream()
                .map(s1 -> words.stream()
                        .filter(s2 -> isAnagram.apply(s1, s2))
                        .count())
                .collect(Collectors.toList())
        );
        words.stream()
//                .sorted()
                .filter(s1 -> !alreadyFound.contains(s1) amp;amp; words.stream()
                        .filter(s2 -> isAnagram.apply(s1, s2))
                        .count() == maxAnagrams)
//                .sorted()
                .forEach(s1 -> {
                    alreadyFound.add(s1);
                    words.stream()
                            .filter(s2 -> isAnagram.apply(s1, s2))
                            .forEach(s2 -> {
                                alreadyFound.add(s2);
                                System.out.print(" "   s2);
                            });
                    System.out.println();
                });
    }
}
  

// РЕДАКТИРОВАТЬ: НЕ ПО ТЕМЕ
Я считаю, что это лучший способ достичь желаемых результатов:

 import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class Main4 {
    public static void main(String[] args) {
        long start = System.currentTimeMillis();

        try (InputStream inputStream = new URL("http://wiki.puzzlers.org/pub/wordlists/unixdict.txt").openStream();
             InputStreamReader inputStreamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
             Stream<String> stream = new BufferedReader(inputStreamReader).lines()) {
            List<List<String>> anagrams = new ArrayList<>(stream
                    .collect(Collectors.groupingBy(o -> {
                        char[] chars = o.toCharArray();
                        Arrays.sort(chars);
                        return new String(chars);
                    }))
                    .values());
            int maxAnagrams = anagrams.parallelStream()
                    .mapToInt(List::size)
                    .max()
                    .orElse(0);
            anagrams.stream()
                    .filter(strings -> strings.size() == maxAnagrams)
                    .sorted(Comparator.comparing(o -> o.get(0)))
                    .forEach(strings -> {
                        strings.forEach(s -> System.out.print(s   " "));
                        System.out.println();
                    });
        } catch (IOException e1) {
            e1.printStackTrace();
        }
        long stop = System.currentTimeMillis();
        System.out.println(stop - start);
    }
}
  

Комментарии:

1. Если бы я мог правильно посчитать скобки, у вас есть поток внутри потоковой операции, и я также вижу 2 System.out . Чего вы ожидаете в противном случае?

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

3. Я полагаю, что в вашем фильтре что-то происходит, что заставляет его удалять дубликаты, если вы сортируете его первым, но во втором этого не происходит. Я немного поиграл с этим, чтобы переписать его, но я получаю то же дублирование, которое вы видите.

4. Вы создали код, полагающийся на порядок обработки потока; ваш предикат filter предполагает, что он может предполагать, что предыдущий элемент уже был обработан forEach . Это нарушено по определению и, как вы можете видеть, будет нарушаться на практике в некоторых сценариях, например, при использовании sorted() или параллельного потока. Кроме того, ваш код, выполняющий повторяющиеся линейные поиски, даже избыточно, ужасно неэффективен. Как правило, вам следует пересмотреть свою привычку использовать forEach для всего, начиная с того, как собирать поток в List

5. Спасибо за все ответы, что касается «эффективности», я добавил лучшее решение для этой проблемы 🙂 Что касается проблемы, описанной ниже, алгоритм, представленный выше, чувствителен к такой операции, sorted() которая выполняет обход потока.

Ответ №1:

Я не уверен на 100%, но я полагаю, что причина, по которой вы видите некоторые из этих действий в зависимости от того, где вы сортируете свой поток, заключается в «синхронизации» данных, проходящих через поток.

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

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

При всем сказанном, если вы пытаетесь избежать дублирования, есть способы получше (у stream есть .distinct() функция, которая сделает это за вас).

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

 abel able bale bela elba 
alger glare lager large regal 
angel angle galen glean lange 
caret carte cater crate trace 
elan lane lean lena neal 
evil levi live veil vile 
  

Если вы хотите, я могу поделиться этим с вами — но у меня складывается впечатление, что это для какого-то назначения, и вы пытаетесь разобраться в этом самостоятельно. Молодец, если это так.

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

Комментарии:

1. Спасибо за ответ, я уже нашел «лучший» (я думаю, по эффективности) алгоритм для этой проблемы и прикрепил его к основному сообщению. Я все еще отчасти не понимаю, как работают потоки внизу (мне просто нужно разобраться с этим), но после прочтения некоторых документов Java ( docs.oracle.com/javase/8/docs/api/java/util/stream / … ) и «примитивной» отладки кажется, что ваш вывод верен.

2. В документах мы можем видеть, что sorted() это a stateful intermediate operation , который описывается как: > Операциям с сохранением состояния может потребоваться обработать весь ввод перед выдачей результата. Например, невозможно получить какие-либо результаты от сортировки потока, пока не будут просмотрены все элементы потока. Таким образом, он блокирует обход для foreach() , и в моем втором случае это дает странные результаты.