使用比较器按特定顺序对对象列表进行排序

Posted

技术标签:

【中文标题】使用比较器按特定顺序对对象列表进行排序【英文标题】:Sorting a list of objects in a specific sequence using comparator 【发布时间】:2021-11-06 23:50:21 【问题描述】:

我遇到了一个让我感到困惑的问题。我有一堂课:

class Order
   private long customerId;
   private long uniqueId;

现在假设我们有 3 个客户的总共 9 个订单对象。每个客户三个订单。所有这些对象都在一个列表中。

我需要使用 Comparator 对该列表进行排序,以便列表首先包含每个客户 ID 的最大唯一 ID。然后是每个客户 ID 的第二大唯一 ID,依此类推。

最终列表示例:

    order(customerId:1, uniqueId:87) order(customerId:2, uniqueId:22) order(customerId:3, uniqueId:57) order(customerId:1, uniqueId:66) order(customerId:2, uniqueId:-4) order(customerId:3, uniqueId:41) order(customerId:1, uniqueId:10) order(customerId:2, uniqueId:-11) order(customerId:3, uniqueId:10)

如何在 'int compare(o1, o2)' 方法中实现此功能以根据要求对列表进行排序?

还有其他更好的方法来实现这一点吗?

【问题讨论】:

不确定这是否适用于单个比较函数,至少不是没有显着开销(对于每个项目一次又一次地检查它的客户所在的位置)。相反,我建议 (a) 按客户拆分项目,(b) 按 ID 排序,降序,(c) 合并列表。 【参考方案1】:

单个compare 方法可能不适合在这里使用。单个Order 中的信息不足以确定其相对顺序。你必须在compare 中做的是:

获取具有相同客户 ID 的所有订单,并按订单 ID 降序排列 为另一个订单做同样的事情 比较排名,如果相同,则比较客户 ID 本身

而且您必须对要比较的每个对订单重复此操作。

相反,您应该将其设为多阶段流程:

首先,按客户 ID 拆分订单,例如使用Collector.groupingBy 按订单 ID 分别对每个批次进行排序 使用两个嵌套循环将它们合并在一起,方法是为第一个客户获取第一个订单,为第二个客户获取第一个订单,等等;确保处理不同客户的订单数量不同的情况

或者,您可以结合第一种和第二种方法,而不是最后的合并步骤,这可能是上述最棘手的部分:

按客户对订单进行分组、排序、在地图中存储每个订单的“排名” 使用 compare 函数对所有订单进行排序,比较这些排名(并将客户 ID 作为平局)

这是最后(混合)方法的一些 Java 代码。不是很漂亮;好久没做Java了,找不到第二步的好办法。

// group by CustomerId
Map<Long, List<Order>> byCustomer = orders.stream()
        .collect(Collectors.groupingBy(Order::getCustomerId));

// build Map of Ranks per CustomerId
Map<Order, Integer> ranks = new HashMap<>();
for (List<Order> group : byCustomer.values()) 
    group.sort(Comparator.comparing(Order::getUniqueId).reversed());
    for (int i = 0; i < group.size(); i++) 
        ranks.put(group.get(i), i);
    


// sort by Rank, then by CustomerId
List<Order> orderedOrders = orders.stream()
        .sorted(Comparator.comparing((Order o) -> ranks.get(o))
                .thenComparing(Order::getCustomerId))
        .collect(Collectors.toList());

【讨论】:

【参考方案2】:

这将使用Guava 及其TreeMultimap 完成工作

    import com.google.common.collect.Ordering;
    import com.google.common.collect.TreeMultimap;

    import java.util.*;
    [...]
    List<Order> orders = new ArrayList<>();
    Order o1 = new Order(1, 87);
    Order o2 = new Order(2, 22);
    Order o3 = new Order(3, 57);
    Order o4 = new Order(1, 66);
    Order o5 = new Order(2, 4);
    Order o6 = new Order(3, 41);
    Order o7 = new Order(1, 10);
    Order o8 = new Order(2, 11);
    Order o9 = new Order(3, 10);
    orders.add(o1);
    orders.add(o2);
    orders.add(o3);
    orders.add(o4);
    orders.add(o5);
    orders.add(o6);
    orders.add(o7);
    orders.add(o8);
    orders.add(o9);

    // Use a treemap to order automatically uniqueId and customer
    // Map[customId => list of uniqueId ordered...]
    TreeMultimap<Long, Long> treeMap = TreeMultimap.create(Ordering.natural(), Ordering.natural());
    orders.forEach(order -> treeMap.put(order.getCustomerId(), order.getUniqueId()));

    System.out.println(treeMap); 
    // output 1=[10, 66, 87], 2=[4, 11, 22], 3=[10, 41, 57]

    // Will be used to know when all Orders are sorted
    boolean allCleared = false;

    // The result list
    List<Order> ordersOrdered = new ArrayList<>();

    while (!allCleared) 
        for (Long customerId : treeMap.keySet()) 
            SortedSet<Long> uniqueIds = treeMap.get(customerId);
            // We retrieve the Order to sort and we add it to the ordered list
            orders.stream()
                    .filter(order -> order.getCustomerId() == customerId && order.getUniqueId() == uniqueIds.last())
                    .findFirst()
                    .ifPresent(ordersOrdered::add);
        

        // Once an Order has been sorted, we remove it from the map
        ordersOrdered.forEach(order -> treeMap.remove(order.getCustomerId(), order.getUniqueId()));

        // As long as we have Order to sort, we continue
        allCleared = treeMap.asMap().size() == 0;
    

    System.out.println(ordersOrdered);
    // [OrdercustomerId=1, uniqueId=87,
    // OrdercustomerId=2, uniqueId=22,
    // OrdercustomerId=3, uniqueId=57,
    // OrdercustomerId=1, uniqueId=66,
    // OrdercustomerId=2, uniqueId=11,
    // OrdercustomerId=3, uniqueId=41,
    // OrdercustomerId=1, uniqueId=10,
    // OrdercustomerId=2, uniqueId=4,
    // OrdercustomerId=3, uniqueId=10]

注意如果您不能使用Treemultimap,您可以将其替换为经典的Map&lt;Long, List&lt;Long&gt;&gt; sortedMap = new TreeMap&lt;&gt;(Long::compare),并使用list.sort(Long::compareTo); 对不同列表进行排序。

【讨论】:

TreeMultimap 不包含在标准java API中,最好添加导入并声明依赖。【参考方案3】:

嗯,这就是我想出的。我分两个阶段完成。

首先创建一个TreeMap&lt;Long,List&lt;Order&gt;&gt;,其中列表根据uniqueID按降序排列。 然后遍历映射值并提取每个 List 元素的相同位置。先是 1,然后是 2,...

使用记录来简化示例。

record Order(long getCustomerId, long getUniqueId) 
    @Override
    public String toString() 
        return String.format("Cid = %s, Uid = %s", getCustomerId,
                getUniqueId);
    

数据和随机播放以确保随机化。添加了额外的值来为每个CustomerId创建不同数量的订单

List<Order> orders = new ArrayList<>(List.of(new Order(1, 87),
        new Order(2, 22), new Order(3, 57), new Order(1, 66),
        new Order(2, -11), new Order(3, 98), new Order(2, -4),
        new Order(3, 41), new Order(1, 10), new Order(2, -14),
        new Order(3, 10)));

Collections.shuffle(orders);

现在创建地图

按相反顺序排序,唯一 ID 在执行groupingBy 时,每个列表将按照遇到的顺序构建,保留已排序的列表。由于这是一个TreeMap,因此每个列表都将按照与键相关的正确顺序。
Map<Long, List<Order>> map = orders.stream()
        .sorted(Comparator.comparing(Order::getUniqueId)
                .reversed())
        .collect(Collectors.groupingBy(Order::getCustomerId,
                TreeMap::new,
                Collectors.toList()));

现在是最终列表的创建

首先迭代一个从 0 开始的索引。这是针对每个列表的 keys 不再重要,所以只需要值(即List&lt;Order&gt;) 随着每个索引的迭代,每个列表中的 Order 条目被映射到一个流 以及随后的新列表。 由于每个CustomerId 的订单数量可能不同,因此会根据索引检查大小以过滤掉较短的列表。否则会抛出 IndexOutOfBounds 异常。 然后通过takeWhile 检查新列表的大小。如果为空,则所有元素都已处理。然后,所有这些列表都按要求的顺序将flatMapped 转换为单个返回的List&lt;Order&gt;
List<Order> result = Stream.iterate(0, i -> i + 1)
        .map(i -> map.values().stream()
                .filter(lst -> lst.size() > i)
                .map(lst -> lst.get(i)).toList())
        .takeWhile(lst -> lst.size() > 0)
        .flatMap(List::stream).toList();

result.forEach(System.out::println);

打印

Cid = 1, Uid = 87
Cid = 2, Uid = 22
Cid = 3, Uid = 98
Cid = 1, Uid = 66
Cid = 2, Uid = -4
Cid = 3, Uid = 57
Cid = 1, Uid = 10
Cid = 2, Uid = -11
Cid = 3, Uid = 41
Cid = 2, Uid = -14
Cid = 3, Uid = 10

【讨论】:

以上是关于使用比较器按特定顺序对对象列表进行排序的主要内容,如果未能解决你的问题,请参考以下文章

Java 8 进阶手册(XX):使用 Comparator 对列表进行排序

Java 8 进阶手册(XX):使用 Comparator 对列表进行排序

按字母顺序对对象的 ArrayList 进行排序

Java 8:按属性对对象列表进行排序,无需自定义比较器

按特定顺序对名称数组进行排序

如何按 C# 中的特定字段对对象列表进行排序?