Java 8 改进Java 8查找“中最常见单词的方法”;“战争与和平”;

Java 8 改进Java 8查找“中最常见单词的方法”;“战争与和平”;,java-8,java-stream,Java 8,Java Stream,我在理查德·伯德(Richard Bird)的书中读到了这个问题:在(或任何其他文本)中找到最常见的五个单词 以下是我目前的尝试: public class WarAndPeace { public static void main(String[] args) throws Exception { Map<String, Integer> wc = Files.lines(Paths.get("/tmp", "/war-and-pea

我在理查德·伯德(Richard Bird)的书中读到了这个问题:在(或任何其他文本)中找到最常见的五个单词

以下是我目前的尝试:

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

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


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

您正在重新编译每一行和每一个单词上的所有正则表达式。代替
.flatMap(line->Arrays.stream(line.split(\\s+)))
编写
.flatMap(Pattern.compile(\\s+):“splitAsStream”)
。对于
.filter(word->word.matches(\\w+))
:使用
.filter(Pattern.compile(“^\\w+$”).asPredicate())
。对于
地图
也一样

可能最好交换
.map(s->s.toLowerCase())
.filter(s->s.length()>=2)
以避免调用
toLowerCase()
一个字母的单词

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

您可以使用ready comparator:
Map.Entry.comparingByValue()
(尽管可能是口味问题),而不是
(e1,e2)->Integer.compare(e2.getValue(),e1.getValue())

总结如下:

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()));
Map-wc=
Files.lines(path.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(收集器.分组方式(w->w),
收集器。总和(w->1));
wc.entrySet()
.stream()
.sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
.限额(5)
.forEach(e->System.out.println(e.getKey()+“:”+e.getValue());

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

您正在执行一些冗余和不必要的操作

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

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

  • 选择正确的
    收集器
    。原则上,有
    collector.counting()
    collector.summingit(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()));

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

有趣的问题,尽管可能更多的是针对代码检查而不是堆栈溢出,我建议您在代码检查时问这个问题:类似的代码。检查splitAsStream。哈哈,或者只是阅读tagir的答案。你有好的观点,我做了一些关于装箱开销的测试,在我的测试中,在finisher收集器中装箱比在每个步骤收集器中装箱更快。我发现自己对
collector.counting()
基于装箱
reduce
感到惊讶。事实上,
Collectors.groupingBy(w->w,Collectors.counting())
的性能甚至比
Collectors.toMap(w->w,w->1L,Long::sum)
还要差,尽管两者都在装箱所有值(可能是由于每个组的第一项处理方式不同,或者只是抖动)。但最终,在模式匹配方面还有更多的改进空间…@Holger,至于计数,这是我第一次接受的补丁。不幸的是,这样的性能补丁很少进行后端口…讨论得很好!非常感谢。我需要更多的实践思维,在各地的溪流。谢谢你的分析!好几点。我很难接受答案。
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()));