使用Stream.collect(groupingBy(identity(), counting())对结果进行分组,并按值排序。

11

我可以将单词列表收集到一个袋子中

(也称为多集合):(参见链接)
Map<String, Long> bag =
        Arrays.asList("one o'clock two o'clock three o'clock rock".split(" "))
        .stream()
        .collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));

然而,袋子中的条目没有任何特定顺序的保证。例如,

{rock=1, o'clock=3, one=1, three=1, two=1}

我可以将它们放入一个列表中,然后使用我的值比较器实现对它们进行排序:

ArrayList<Entry<String, Long>> list = new ArrayList<>(bag.entrySet());
Comparator<Entry<String, Long>> valueComparator = new Comparator<Entry<String, Long>>() {

    @Override
    public int compare(Entry<String, Long> e1, Entry<String, Long> e2) {
        return e2.getValue().compareTo(e1.getValue());
    }
};
Collections.sort(list, valueComparator);

这将得到所需的结果:

[o'clock=3, rock=1, one=1, three=1, two=1]

有没有更优雅的方法来处理这个问题?我相信很多人都解决过这个问题。Java Streams API 中是否内置了我可以使用的东西?


2
请查看 Pattern.splitAsStream。 - Brian Goetz
谢谢@Brian。我不知道Pattern.splitAsStream方法。 - whistling_marmot
2个回答

13

您不需要创建比较器,因为这个任务已经有了一个: Map.Entry.comparingByValue。它会创建一个比较器来比较映射条目的值。在这种情况下,我们对它们进行逆序排序,所以可以使用:

Map.Entry.comparingByValue(Comparator.reverseOrder())

作为比较器。那么您的代码可能会变成

Collections.sort(list, Map.Entry.comparingByValue(Comparator.reverseOrder()));

如果没有自定义比较器,可以使用流水线对生成的Map按值进行排序。另外,如果要处理长字符串,不建议调用Stream.of(Arrays.asList("...").split(" ")),而是应该调用Pattern.compile(" ").splitAsStream("...")

Map<String, Long> bag =
   Pattern.compile(" ")
          .splitAsStream("one o'clock two o'clock three o'clock rock")
          .collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
Map<String, Long> sortedBag = 
    bag.entrySet()
       .stream()
       .sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
       .collect(Collectors.toMap(
           Map.Entry::getKey,
           Map.Entry::getValue,
           (v1, v2) -> { throw new IllegalStateException(); },
           LinkedHashMap::new
       ));

此代码创建了一个 Map 条目的流,按照值的逆序进行排序,并将其收集到 LinkedHashMap 中以保持遇到的顺序。

输出:

{o'clock=3, rock=1, one=1, three=1, two=1}

或者,您可以考虑使用StreamEx库,您可以得到以下效果:


Map<String, Long> bag =
    StreamEx.split("one o'clock two o'clock three o'clock rock", " ")
            .sorted()
            .runLengths()
            .reverseSorted(Map.Entry.comparingByValue())
            .toCustomMap(LinkedHashMap::new);

这段代码对每个字符串进行排序,然后调用runLengths()方法。此方法将相邻的相同元素合并成一个Stream<String, Long>,其中值是元素出现的次数。例如,在流["foo", "foo", "bar"]上,该方法将生成流[Entry("foo", 2), Entry("bar", 1)]。最后,按值的降序排序并收集到LinkedHashMap中。

请注意,这样可以在不必执行两个不同的流管道的情况下获得正确的结果。


1
请注意,StreamEx解决方案(带有runLengths)在大型输入上可能会更慢。当数据已经预先排序(或者您不需要排序)时,它的效果最好。根据我的测试,显式排序步骤较慢。另一方面,我优化了像“”这样的单字符正则表达式,因此通过StreamEx进行拆分将更快。 - Tagir Valeev
有没有办法摆脱第二个流管道?第一个袋子只是我们不需要的中间结果。 - Roland
1
@Roland StreamEx解决方案在单个管道中运行,但中间映射的创建确实是必要的:为了按映射的值进行排序,所有条目首先都需要存在。 - Tunaki

4

如果您愿意使用已经内置了Bag类型的第三方库,那么您可以使用Eclipse Collections来执行以下操作:

Bag<String> bag =
    Bags.mutable.with("one o'clock two o'clock three o'clock rock".split(" "));
ListIterable<ObjectIntPair<String>> pairs = bag.topOccurrences(bag.sizeDistinct());
Assert.assertEquals(PrimitiveTuples.pair("o'clock", 3), pairs.getFirst());
Assert.assertEquals(PrimitiveTuples.pair("rock", 1), pairs.getLast());
System.out.println(pairs);

这的输出结果是:
[o'clock:3, two:1, one:1, three:1, rock:1]

尽管订单的价值被排序,但当存在并列时,键没有可预测的顺序。如果您想要键有可预测的顺序,可以使用SortedBag

Bag<String> bag =
    SortedBags.mutable.with("one o'clock two o'clock three o'clock rock".split(" "));
ListIterable<ObjectIntPair<String>> pairs = bag.topOccurrences(bag.sizeDistinct());
Assert.assertEquals(PrimitiveTuples.pair("o'clock", 3), pairs.getFirst());
Assert.assertEquals(PrimitiveTuples.pair("two", 1), pairs.getLast());
System.out.println(pairs);

这的输出结果是:
[o'clock:3, one:1, rock:1, three:1, two:1]

如果您想使用Brian建议的Pattern.splitAsStream,则可以按如下方式更改代码,以使用Collector.toCollection处理流:
Bag<String> bag =
    Pattern.compile(" ").splitAsStream("one o'clock two o'clock three o'clock rock")
        .collect(Collectors.toCollection(TreeBag::new));
ListIterable<ObjectIntPair<String>> pairs = bag.topOccurrences(bag.sizeDistinct());
Assert.assertEquals(PrimitiveTuples.pair("o'clock", 3), pairs.getFirst());
Assert.assertEquals(PrimitiveTuples.pair("two", 1), pairs.getLast());
System.out.println(pairs);

注意:我是Eclipse Collections的提交者。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接