Stream.collect(groupingBy(identity(),counting())然后按值对结果进行排序

时间:2016-01-18 16:46:07

标签: java java-8 java-stream

我可以collect a list of words into a bag(a.k.a. multi-set):

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 个答案:

答案 0 :(得分:7)

您无需创建比较器,此任务已有一个:Map.Entry.comparingByValue。这将创建一个比较器,用于比较地图的条目值。在这种情况下,我们对它们的逆序感兴趣,所以我们可以:

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

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

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

没有自定义比较器。

要对结果Map进行排序,您还可以使用Stream管道。另外,如果您要处理长字符串,则可能需要调用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
       ));

此代码创建地图条目的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);

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

请注意,这样可以得到正确的结果,而无需执行2条不同的Stream流水线。

答案 1 :(得分: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来处理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的提交者。