【问题标题】:sorting Lists after groupingBygroupingBy 后排序列表
【发布时间】:2025-12-04 20:30:01
【问题描述】:

我想知道,流(或收集器)中是否已经实现了将列表排序为值的功能。例如。以下代码均生成按年龄排序的按性别分组的人员列表。第一个解决方案有一些开销排序(看起来有点邋遢)。第二种解决方案需要对每个人进行两次检查,但效果很好。

先排序,然后在一个流中分组:

Map<Gender, List<Person>> sortedListsByGender = (List<Person>) roster
        .stream()
        .sorted(Person::compareByAge)
        .collect(Collectors.groupingBy(Person::getGender));

先分组,然后对每个值进行排序:

Map<Gender, List<Person>> sortedListsByGender = (List<Person>) roster
        .stream()
        .collect(Collectors.groupingBy(Person::getGender));
sortedListsByGender.values()
        .forEach(list -> Collections.sort(list, Person::compareByAge));

我只是想知道,是否已经实现了一些东西,它可以一次性完成,例如groupingBySorted

【问题讨论】:

  • 不,没有。 (也就是说——为什么你认为它不能在并行流中工作?当然可以。)
  • @LouisWasserman 你确定吗(评论的第二部分)?查看sortedcollect 的文档,我有一种强烈的预感,这将与并行流式传输中断,但我在这里可能完全错了。随意,纠正我的问题,不想传播错误的信息。
  • 是的,我是。并行性不会干扰流的排序属性。
  • 可能是因为没有内置SortedList:*.com/q/8725387/1743880
  • 您可以使用Collectors.collectingAndThen(groupingBy(Person::getGender), l -&gt; {Collections.sort(l, Person::compareByAge); return l;}); 稍微压缩第二个代码 sn-p,如果您在许多地方存储和重用生成的 Collector,这可能是值得的。但是如果你想边收集边排序,就需要自己写了。这是有道理的,因为在最后构建未排序列表和快速/合并排序可能更快,而不是在收集期间通过将每个元素插入其位置(插入排序)来维护排序列表。

标签: java sorting java-stream groupingby


【解决方案1】:

collect 操作之前在流上使用sorted(comparator) 时,流必须缓冲整个流内容才能对其进行排序,并且与排序相比,排序可能涉及该缓冲区内更多的数据移动之后是较小的组列表。所以性能不如对各个组进行排序,尽管如果启用了并行处理,实现将使用多个内核。

但请注意,使用 sortedListsByGender.values().forEach(…) 不是可并行化的操作,即使使用 sortedListsByGender.values().parallelStream().forEach(…) 也只能允许并行处理组,而每个排序操作仍然是顺序的。

当在收集器中执行排序操作时

static <T> Collector<T,?,List<T>> toSortedList(Comparator<? super T> c) {
    return Collectors.collectingAndThen(
        Collectors.toCollection(ArrayList::new), l->{ l.sort(c); return l; } );
}

 

Map<Gender, List<Person>> sortedListsByGender = roster.stream()
    .collect(Collectors.groupingBy(Person::getGender, toSortedList(Person::compareByAge)));

排序操作的行为相同(感谢 Tagir Valeev 纠正我),但您可以轻松检查插入时排序策略的执行情况。只需将收集器实现更改为:

static <T> Collector<T,?,List<T>> toSortedList(Comparator<? super T> c) {
    return Collectors.collectingAndThen(
        Collectors.toCollection(()->new TreeSet<>(c)), ArrayList::new);
}

为了完整起见,如果您想要一个首先插入排序到ArrayList 的收集器以避免最后的复制步骤,您可以使用更详细的收集器,如下所示:

static <T> Collector<T,?,List<T>> toSortedList(Comparator<? super T> c) {
    return Collector.of(ArrayList::new,
        (l,t) -> {
            int ix=Collections.binarySearch(l, t, c);
            l.add(ix<0? ~ix: ix, t);
        },
        (list1,list2) -> {
            final int s1=list1.size();
            if(list1.isEmpty()) return list2;
            if(!list2.isEmpty()) {
                list1.addAll(list2);
                if(c.compare(list1.get(s1-1), list2.get(0))>0)
                    list1.sort(c);
            }
            return list1;
        });
}

它对于顺序使用很有效,但它的合并功能不是最佳的。底层排序算法将受益于预先排序的范围,但必须首先找到这些范围,尽管我们的合并函数实际上知道这些范围。不幸的是,JRE 中没有公共 API 允许我们使用这些信息(有效;我们可以将 subLists 传递给 binarySearch,但为 list2 的每个元素创建一个新的子列表可能会变得太昂贵) .如果我们想进一步提高并行执行的性能,我们必须重新实现排序算法的合并部分:

static <T> Collector<T,?,List<T>> toSortedList(Comparator<? super T> c) {
    return Collector.of(ArrayList::new,
        (l,t) -> l.add(insertPos(l, 0, l.size(), t, c), t),
        (list1,list2) -> merge(list1, list2, c));
}
static <T> List<T> merge(List<T> list1, List<T> list2, Comparator<? super T> c) {
    if(list1.isEmpty()) return list2;
    for(int ix1=0, ix2=0, num1=list1.size(), num2=list2.size(); ix2<num2; ix2++, num1++) {
        final T element = list2.get(ix2);
        ix1=insertPos(list1, ix1, num1, element, c);
        list1.add(ix1, element);
        if(ix1==num1) {
            while(++ix2<num2) list1.add(list2.get(ix2));
            return list1;
        }
    }
    return list1;
}
static <T> int insertPos(
    List<? extends T> list, int low, int high, T t, Comparator<? super T> c) {
    high--;
    while(low <= high) {
        int mid = (low+high)>>>1, cmp = c.compare(list.get(mid), t);
        if(cmp < 0) low = mid + 1;
        else if(cmp > 0) high = mid - 1;
        else {
            mid++;
            while(mid<=high && c.compare(list.get(mid), t)==0) mid++;
            return mid;
        }
    }
    return low;
}

请注意,与简单的基于 binarySearch 的插入不同,最后一种解决方案是一种稳定的排序实现,即在您的情况下,Persons 具有相同的年龄和 Gender 不会改变它们的相对顺序,如果源流具有定义的相遇顺序。

【讨论】:

  • Erm... AFAIK,在上游整理器执行期间,下游整理器是 executed,对于所有映射条目,上游整理器始终是 executed,在调用者线程中只有一次。 Finisher 永远不会在中间容器上执行。这是行不通的,因为整理器可能会更改对象类型。
  • @Tagir Valeev:你说得对,我太关注合并功能了。
  • 感谢您详尽的回答!我仍在检查表演(这取决于人数(和性别:-))以及是否使用顺序或并行流)。希望其中一些将在未来的版本中实现。