【问题标题】:Why Java 8 stream forEach loop is duplicating results after usage of sorted() method为什么 Java 8 流 forEach 循环在使用 sorted() 方法后重复结果
【发布时间】:2026-02-01 17:00:01
【问题描述】:

在使用字谜练习期间,我发现执行后的代码在输出中存在重复,具体取决于 Stream 类中 sorted() 方法的使用时刻。

当我在filter()forEach() 方法之前进行排序时

        words.stream()
                .sorted()
                .filter(s1 -> !alreadyFound.contains(s1) && 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

但是当我在filter() 之后和forEach() 方法之前使用sorted()

        words.stream()
                .filter(s1 -> !alreadyFound.contains(s1) && 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) && 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);
    }
}

【问题讨论】:

  • 如果我能正确计算括号,你在流操作中有一个流,我也可以看到 2 System.out。否则你会期待什么?
  • 如果你将内心的流线分解成它自己的功能,它可能会让你的生活更轻松。我喜欢 BiFunction 的使用,但是如果没有将过滤条件作为谓词,解析起来有点麻烦。
  • 我相信你的过滤器中发生了一些事情,如果你先对它进行排序,它会导致它删除重复项 - 但在第二个没有发生的情况下。我正在尝试重新编写它,但我得到了与您看到的相同的重复项。
  • 您已经根据流的处理顺序创建了代码;您的过滤谓词假定它可以假定前一个元素已由forEach 处理。这在定义上是被破坏的,正如你所看到的,在某些情况下会在实践中被破坏,比如在使用sorted() 或并行流时。此外,您的代码进行重复的线性搜索,即使是冗余的,效率也非常低。通常,您应该重新考虑使用forEach 处理所有事情的习惯,从如何将流收集到List 开始......
  • 感谢所有回复,关于“效率”,我已经为这个问题添加了更好的解决方案:) 关于下面描述的问题,上面介绍的算法对于像 sorted() 这样的操作很敏感,它可以遍历流。

标签: java foreach java-stream


【解决方案1】:

我不能 100% 确定,但我相信您看到这种行为的原因取决于您对流进行排序的位置,这是因为流经流的数据的“时间”。

您的 foreach 正在将它们添加到已找到的单词列表中 - 但请记住,在调用 foreach 之前不会评估任何数据。这意味着这些项目仅在整个集合流经 foreach 时才被添加到列表中。在 foreach 之前发生的主过滤器可能会看到一些重复,因为它尝试按尚不存在的列表进行过滤。

排序是对这一切的阻碍,因为它可以阻止数据流——在看到每个元素之前,不能保证对列表进行排序。这不完全是一个终止操作,但它确实会延迟事情,直到所有元素都通过。懒惰的评估是一件很棒的事情,但它可能很棘手,而且在你走这条路之前,你通常真的需要确保像排序这样的东西是你想要的。

话虽如此,如果您想避免重复,有更好的方法来解决它(流有一个 .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 给我,我很乐意为您提供帮助。之后,我将编辑这篇文章并发布我用来生成上述块的代码。

【讨论】:

  • 感谢您的回复,我已经找到了解决这个问题的“最佳”(我猜是效率)算法并将其附在主帖中。我仍然有点不明白下面的流是如何工作的(我只需要解决它),但是在阅读了一些 Java 文档(docs.oracle.com/javase/8/docs/api/java/util/stream/…)和“原始”调试之后,您的结论似乎是正确的。
  • 在文档中我们可以看到sorted()a stateful intermediate operation,描述为: > 有状态的操作可能需要在产生结果之前处理整个输入。例如,在查看流的所有元素之前,无法通过对流进行排序产生任何结果。所以它阻止了foreach() 的遍历,在我的第二种情况下,这会产生奇怪的结果。