【问题标题】:Improving the Java 8 way of finding the most common words in "War and Peace"改进 Java 8 查找“战争与和平”中最常用词的方法
【发布时间】:2016-03-11 03:57:43
【问题描述】:

我在 Richard Bird 的书中读到了这个问题:在War and Peace(或任何其他文本)中找到最常用的五个词

这是我目前的尝试:

public class WarAndPeace {
    public static void main(String[] args) throws Exception {
        Map<String, Integer> wc =
            Files.lines(Paths.get("/tmp", "/war-and-peace.txt"))
            .map(line -> line.replaceAll("\\p{Punct}", ""))
            .flatMap(line -> Arrays.stream(line.split("\\s+")))
            .filter(word -> word.matches("\\w+"))
            .map(s -> s.toLowerCase())
            .filter(s -> s.length() >= 2)
            .collect(Collectors.toConcurrentMap(
                    w -> w, w -> 1, Integer::sum));

        wc.entrySet()
            .stream()
            .sorted((e1, e2) -> Integer.compare(e2.getValue(), e1.getValue()))
            .limit(5)
            .forEach(e -> System.out.println(e.getKey() + ": " + e.getValue()));

    }
}

这绝对看起来很有趣并且运行速度相当快。在我的笔记本电脑上打印以下内容:

$> time java -server -Xmx10g -cp target/classes tmp.WarAndPeace
the: 34566
and: 22152
to: 16716
of: 14987
a: 10521
java -server -Xmx10g -cp target/classes tmp.WarAndPeace  1.86s user 0.13s system 274% cpu 0.724 total

它通常在 2 秒内运行。您能否从表现力和性能的角度提出进一步的改进建议?

PS:如果您对这个问题的丰富历史感兴趣,请参阅here

【问题讨论】:

标签: java-8 java-stream


【解决方案1】:

您正在重新编译每一行和每个单词的所有正则表达式。而不是.flatMap(line -&gt; Arrays.stream(line.split("\\s+"))).flatMap(Pattern.compile("\\s+")::splitAsStream).filter(word -&gt; word.matches("\\w+")) 也一样:使用 .filter(Pattern.compile("^\\w+$").asPredicate())map 也一样。

可能最好交换.map(s -&gt; s.toLowerCase()).filter(s -&gt; s.length() &gt;= 2),以免调用toLowerCase() 获得一个字母的单词。

您不应使用Collectors.toConcurrentMap(w -&gt; w, w -&gt; 1, Integer::sum)。首先,您的流不是并行的,因此您可以轻松地将toConcurrentMap 替换为toMap。其次,使用Collectors.groupingBy(w -&gt; w, Collectors.summingInt(w -&gt; 1)) 可能会更有效(尽管测试是必要的),因为这会减少装箱(但添加一个将一次装箱所有值的完成步骤)。

您可以使用现成的比较器来代替(e1, e2) -&gt; Integer.compare(e2.getValue(), e1.getValue())Map.Entry.comparingByValue()(尽管这可能是个人喜好问题)。

总结一下:

Map<String, Integer> wc =
    Files.lines(Paths.get("/tmp", "/war-and-peace.txt"))
        .map(Pattern.compile("\\p{Punct}")::matcher)
        .map(matcher -> matcher.replaceAll(""))
        .flatMap(Pattern.compile("\\s+")::splitAsStream)
        .filter(Pattern.compile("^\\w+$").asPredicate())
        .filter(s -> s.length() >= 2)
        .map(s -> s.toLowerCase())
        .collect(Collectors.groupingBy(w -> w,
                Collectors.summingInt(w -> 1)));

wc.entrySet()
    .stream()
    .sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
    .limit(5)
    .forEach(e -> System.out.println(e.getKey() + ": " + e.getValue()));

如果您不喜欢方法引用(有些人不喜欢),您可以将预编译的正则表达式存储在变量中。

【讨论】:

  • 你说得很好,我做了一些关于拳击开销的测试,在我的测试中,装箱完成器收集器比装箱每步收集器更快。我发现自己对Collectors.counting() 基于拳击reduce 感到惊讶。事实上,Collectors.groupingBy(w-&gt;w, Collectors.counting()) 的表现甚至比Collectors.toMap(w -&gt; w, w -&gt; 1L, Long::sum) 更差,尽管两者都对所有值进行了装箱(可能是由于每个组的第一项的处理方式不同,或者只是抖动)。但归根结底,模式匹配还有很大的改进空间……
  • @Holger,至于计数,那是我第一个接受JDK-9 的补丁。不幸的是,这样的性能补丁很少被向后移植......
  • 很好的讨论!谢谢你。我需要更多练习思考无处不在的流。
【解决方案2】:

您正在执行几个冗余和不必要的操作。

  • 首先用空字符串替换所有标点字符,创建新字符串,然后使用空格字符作为边界执行拆分操作。这甚至有合并由标点符号分隔的单词而没有空格的风险。您可以通过用空格替换标点符号来解决这个问题,但最后,您不需要替换,因为您可以将拆分模式更改为“标点符号或空格”,但是
  • 然后,您将通过接受仅由单词字符组成的字符串来过滤拆分结果。由于您已经删除了所有标点和空格字符,这将整理出既不是单词、空格或标点字符的字符串,我不确定这是否是预期的逻辑。毕竟,如果您只对单词感兴趣,为什么不首先只搜索单词呢?由于 Java 8 不支持匹配流,我们可以指导它使用非单词字符作为边界进行拆分。

  • 那么你正在做一个.map(s -&gt; s.toLowerCase()).filter(s -&gt; s.length() &gt;= 2)。由于英文文本,改成大写后字符串长度不会改变,不影响过滤条件,所以我们可以先过滤,对不接受的字符串跳过toLowerCase转换通过谓词:.filter(s -&gt; s.length() &gt;= 2).map(s -&gt; s.toLowerCase())。净收益可能很小,但不会造成伤害。

  • 选择正确的CollectorTagir already explained it。原则上,Collectors.counting()Collectors.summingInt(w-&gt;1) 更适合,但不幸的是,Oracle 目前的实现很差,因为它基于reduce,对所有元素取消装箱和重新装箱Longs。

把它们放在一起,你会得到:

Files.lines(Paths.get("/tmp", "/war-and-peace.txt"))
    .flatMap(Pattern.compile("\\W+")::splitAsStream)
    .filter(s -> s.length() >= 2)
    .map(String::toLowerCase)
    .collect(Collectors.groupingBy(w->w, Collectors.summingInt(w->1)))
    .entrySet()
    .stream()
    .sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
    .limit(5)
    .forEach(e -> System.out.println(e.getKey() + ": " + e.getValue()));

如上所述,如果字数略高于您的方法,请不要感到惊讶。

【讨论】:

  • 感谢您的分析!几个好点。我很难接受答案。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-09-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-04-02
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多