【问题标题】:Stream.collect(groupingBy(identity(), counting()) and then sort the result by valueStream.collect(groupingBy(identity(),counting()) 然后按值对结果进行排序
【发布时间】:2016-01-18 16:46:06
【问题描述】:

我可以collect a list of words into a bag(又名多集):

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 中内置的东西吗?

【问题讨论】:

  • 查看 Pattern.splitAsStream。
  • 谢谢@Brian。我不知道 Pattern.splitAsStream 方法。

标签: java java-8 java-stream


【解决方案1】:

您不需要创建一个比较器,这个任务已经有一个比较器:Map.Entry.comparingByValue。这将创建一个比较器来比较映射条目的值。在这种情况下,我们对它们的相反顺序感兴趣,因此我们可以:

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

作为比较器。你的代码可能会变成

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

没有自定义比较器。


要根据其值对生成的Map 进行排序,您还可以使用 Stream 管道。此外,如果您有长字符串要处理,您可能需要调用Pattern.compile(" ").splitAsStream("..."),而不是调用Stream.of(Arrays.asList("...").split(" "))

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

此代码创建地图条目的 Stream,按值的相反顺序对其进行排序,并将其收集到 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&lt;String, Long&gt;,其中值是元素出现的次数。例如,在流 ["foo", "foo", "bar"] 上,此方法将生成流 [Entry("foo", 2), Entry("bar", 1)]。最后,这是按值的降序排序并收集到LinkedHashMap

请注意,这会给出正确的结果,而无需执行 2 个不同的 Stream 管道。

【讨论】:

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

如果您愿意使用内置 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 使用 Streams:

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 的提交者。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-01-25
    • 1970-01-01
    • 2012-04-12
    • 2018-07-25
    相关资源
    最近更新 更多