在 Java 中使用 Sets 收集独特的和最新的对象

Collect unique and the newest objects with Sets in Java

假设我有一个 class(响应模型)

TransactionResult {
    private List<Transaction> transactions;
}

Transaction {
    private OffsetDateTime date;
    private String account;
}

并且我希望按帐户进行独特的交易并且应该是最新的。所以,我映射了 dto

TransactionResultDto {
    private Set<TransactionDto> transactions;
}

TransactionDto implements Comparable<TransactionDto> {
    private OffsetDateTime date;
    private String account;
    
    //equals and hashcode only using account
    //compareTo using date
}

首先,我收集到一个 TreeSet 中以对交易进行排序(最新的排在第一位),然后我构建一个 HashSet 以按帐户具有唯一的交易。

Set<TransactionDto> transactions = new HashSet<>(
                transactionResult.getTransactions().stream()
                        .map(transaction -> mapToTransactionDto(transaction))
                        .collect(Collectors.toCollection(TreeSet::new)));

所以,问题是:

  1. 这是按帐户交易收集最新且唯一的正确方法吗?
  2. 您有什么改进或其他想法吗?

1。不,在这种情况下使用 TreeSet 是不正确的。

假设我们有交易:
{"account": "A", "date": "2022-01-01"},
{"account": "B", "date": "2022-01-01"}(为简单起见省略时间)。

如果将它们放在 TreeSet 中,它们将被视为同一对象,因为您只比较 date。(即,如果将两者都放在 TreeSet 中,则只剩下 1 个对象)。 您可能面临的是,当 date 与另一个帐户冲突时,某些帐户记录可能会消失。它可能非常罕见,但如果真的发生,您将花费数天时间进行调试。

2。让我们修复它并使其更具描述性

你的方法也可以用sort固定,然后放到HashSet。但是,这种方法有一些缺点:

  • 逻辑并不简单,因为 reader 需要时间来弄清楚它要实现的目标。
  • 覆盖 hashcodeequals 以仅考虑 account 可能会在其他用户期望同时考虑 accountdate 时引起麻烦。

如你所愿

  1. 分组account
  2. select 笔交易,每组最多 date

下游我们可以使用Collectors#groupingBy and then select Collectors#maxBy,如下图:

Set<TransactionDto> transactions = transactionResult.getTransactions().stream().map(TransactionDto::new)
       .collect(
               groupingBy(TransactionDto::getAccount,
                       collectingAndThen(
                               maxBy(Comparator.comparing(TransactionDto::getDate)),
                               Optional::get)
               )
       ).values().stream()
       .collect(toSet());

我可能会一次遍历它们。您可以使用 HashMap 为每个帐户存储最新的,然后从 HashMap 的值构建一个集合。

你问了其他提示,所以我会:

  1. 有一种方法可以找到您想要的交易。转换为 DTO 或其他任何东西都可以在别处处理。
  2. 让 equals 和 hash 使用所有字段,否则它对其他用例没有用。例如,我在下面为我的方法编写了一些单元测试,使用所有字段的 equals 和 hash 非常有用。
  3. 我个人不会在这里为此任务实施比较器。你可以想象其他有用的方法来为不同的任务排序这些,我不认为这个对象真的有一个内在的顺序。您始终可以根据需要实现任意数量的自定义比较器,但我个人不会为此任务这样做。
  4. 在不太了解您的系统架构的情况下,我希望 DTO 用于传输数据和域对象以具有功能,因此我会将逻辑移至那些 classes。您的架构可能不同。

要构建最新交易的集合,我可能会这样做:

private Collection<Transaction> findNewestForEachAccount(Collection<Transaction> transactions) {
    HashMap<String, Transaction> result = new HashMap<>();
    for (Transaction t : transactions) {
        if (isNewestForAccountSoFar(result, t)) {
            result.put(t.getAccount(), t);
        }
    }
    return result.values();
}

private boolean isNewestForAccountSoFar(HashMap<String, Transaction> result, Transaction t) {
    return !result.containsKey(t.getAccount())
        || isNewer(t, result.get(t.getAccount()));
}

private boolean isNewer(Transaction candidate, Transaction incumbent) {
    return candidate.getDate().isAfter(incumbent.getDate());
}

然后您可以像这样构建 DTO:

    List<Transaction> transactions = transactionResult.getTransactions();
    Set<TransactionDto> selectedTransactionDTOs = findNewestForEachAccount(transactions)
        .stream()
        .map(t -> mapToTransactionDto(t))
        .collect(Collectors.toSet());

一种选择是将查找最新交易的逻辑移动到您的 TransactionResult class。这意味着您不必公开整个列表的 getter 。这取决于您的架构,因此您的里程可能会有所不同。