在 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。不,在这种情况下使用 TreeSet
是不正确的。
假设我们有交易:
{"account": "A", "date": "2022-01-01"}
,
{"account": "B", "date": "2022-01-01"}
(为简单起见省略时间)。
如果将它们放在 TreeSet
中,它们将被视为同一对象,因为您只比较 date
。(即,如果将两者都放在 TreeSet
中,则只剩下 1 个对象)。
您可能面临的是,当 date
与另一个帐户冲突时,某些帐户记录可能会消失。它可能非常罕见,但如果真的发生,您将花费数天时间进行调试。
2。让我们修复它并使其更具描述性
你的方法也可以用sort
固定,然后放到HashSet
。但是,这种方法有一些缺点:
- 逻辑并不简单,因为 reader 需要时间来弄清楚它要实现的目标。
- 覆盖
hashcode
和 equals
以仅考虑 account
可能会在其他用户期望同时考虑 account
和 date
时引起麻烦。
如你所愿
- 分组
account
- 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 的值构建一个集合。
你问了其他提示,所以我会:
- 有一种方法可以找到您想要的交易。转换为 DTO 或其他任何东西都可以在别处处理。
- 让 equals 和 hash 使用所有字段,否则它对其他用例没有用。例如,我在下面为我的方法编写了一些单元测试,使用所有字段的 equals 和 hash 非常有用。
- 我个人不会在这里为此任务实施比较器。你可以想象其他有用的方法来为不同的任务排序这些,我不认为这个对象真的有一个内在的顺序。您始终可以根据需要实现任意数量的自定义比较器,但我个人不会为此任务这样做。
- 在不太了解您的系统架构的情况下,我希望 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 。这取决于您的架构,因此您的里程可能会有所不同。
假设我有一个 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。不,在这种情况下使用 TreeSet
是不正确的。
假设我们有交易:
{"account": "A", "date": "2022-01-01"}
,
{"account": "B", "date": "2022-01-01"}
(为简单起见省略时间)。
如果将它们放在 TreeSet
中,它们将被视为同一对象,因为您只比较 date
。(即,如果将两者都放在 TreeSet
中,则只剩下 1 个对象)。
您可能面临的是,当 date
与另一个帐户冲突时,某些帐户记录可能会消失。它可能非常罕见,但如果真的发生,您将花费数天时间进行调试。
2。让我们修复它并使其更具描述性
你的方法也可以用sort
固定,然后放到HashSet
。但是,这种方法有一些缺点:
- 逻辑并不简单,因为 reader 需要时间来弄清楚它要实现的目标。
- 覆盖
hashcode
和equals
以仅考虑account
可能会在其他用户期望同时考虑account
和date
时引起麻烦。
如你所愿
- 分组
account
- 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 的值构建一个集合。
你问了其他提示,所以我会:
- 有一种方法可以找到您想要的交易。转换为 DTO 或其他任何东西都可以在别处处理。
- 让 equals 和 hash 使用所有字段,否则它对其他用例没有用。例如,我在下面为我的方法编写了一些单元测试,使用所有字段的 equals 和 hash 非常有用。
- 我个人不会在这里为此任务实施比较器。你可以想象其他有用的方法来为不同的任务排序这些,我不认为这个对象真的有一个内在的顺序。您始终可以根据需要实现任意数量的自定义比较器,但我个人不会为此任务这样做。
- 在不太了解您的系统架构的情况下,我希望 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 。这取决于您的架构,因此您的里程可能会有所不同。