改进Java 8方式寻找"战争与和平"中最常见的单词

Ked*_*ade 5 java-8 java-stream

我读理查德·伯德的书这样的问题:找到五种最常用的词战争与和平(或任何其他文本为此事).

这是我目前的尝试:

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()));

    }
}
Run Code Online (Sandbox Code Playgroud)

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

$> 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
Run Code Online (Sandbox Code Playgroud)

它通常在2秒内运行.你能从表现力和表现的角度建议进一步改进吗?

PS:如果您对此问题的丰富历史感兴趣,请参阅此处.

Tag*_*eev 10

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

可能最好交换.map(s -> s.toLowerCase()),.filter(s -> s.length() >= 2)以便不要求toLowerCase()一个字母的单词.

你不应该使用Collectors.toConcurrentMap(w -> w, w -> 1, Integer::sum).首先,你流不平行,所以你可以很容易地更换toConcurrentMaptoMap.其次,它可能会更有效(虽然测试是必要的),Collectors.groupingBy(w -> w, Collectors.summingInt(w -> 1))因为这会减少装箱(但添加一个修整器步骤,它将立即包装所有值).

而不是(e1, e2) -> 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()));
Run Code Online (Sandbox Code Playgroud)

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

  • 你有好点,我做了一些关于拳击开销的测试,在我的测试中,拳击收尾者收藏家比拳击收集器更快.我发现自己很惊讶`Collectors.counting()`基于拳击`reduce`.实际上,`Collectors.groupingBy(w-> w,Collectors.counting())`的表现甚至比`Collectors.toMap(w - > w,w - > 1L,Long :: sum)`更糟糕,尽管两者都是拳击值(可能是由于每组的第一项的不同处理或者只是抖动).但最终,模式匹配的改进空间更大...... (2认同)
  • @Holger,至于计数,这是我第一次接受[JDK-9]的补丁(http://hg.openjdk.java.net/jdk9/jdk9/jdk/rev/1edfa4abd77a).不幸的是,这些性能补丁很少被向后移植...... (2认同)

Hol*_*ger 7

您正在执行多项冗余和不必要的操作.

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

  • 然后你正在做一个.map(s -> s.toLowerCase()).filter(s -> s.length() >= 2).因为对于英文文本,字符串长度在将其更改为大写时不会改变,过滤条件不受影响,因此我们可以过滤,跳过toLowerCase谓词不接受的字符串的转换:.filter(s -> s.length() >= 2).map(s -> s.toLowerCase()).净收益可能很小,但不会受到伤害.

  • 选择正确的Collector.Tagir已经解释过了.原则上,Collectors.counting()哪个更适合Collectors.summingInt(w->1),但不幸的是,Oracle的当前实现很差,因为它基于所有元素的reduce拆箱和重新装箱Long.

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

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()));
Run Code Online (Sandbox Code Playgroud)

如上所述,如果单词计数略高于您的方法,请不要感到惊讶.