使用比较器按特定顺序对对象列表进行排序
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 拆分订单,例如使用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<Long, List<Long>> sortedMap = new TreeMap<>(Long::compare)
,并使用list.sort(Long::compareTo);
对不同列表进行排序。
【讨论】:
TreeMultimap
不包含在标准java API中,最好添加导入并声明依赖。【参考方案3】:
嗯,这就是我想出的。我分两个阶段完成。
首先创建一个TreeMap<Long,List<Order>>
,其中列表根据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<Order>
)
随着每个索引的迭代,每个列表中的 Order 条目被映射到一个流
以及随后的新列表。
由于每个CustomerId
的订单数量可能不同,因此会根据索引检查大小以过滤掉较短的列表。否则会抛出 IndexOutOfBounds
异常。
然后通过takeWhile
检查新列表的大小。如果为空,则所有元素都已处理。然后,所有这些列表都按要求的顺序将flatMapped
转换为单个返回的List<Order>
。
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 对列表进行排序