如何基于accountId与invoiceDate字段合并列表中的多个AccountInfo对象
嘿,这个合并需求在业务开发里太常见了!我给你两种靠谱的实现方式,分别适合Java 8+的简洁流处理和传统循环的直观写法,你可以根据项目情况选:
方案一:Java 8+ Stream API 实现(简洁高效)
这种方式用Stream的分组和归约操作,代码简洁,适合已经熟悉Lambda表达式的场景。
首先我们需要一个分组键类,用来把accountId和invoiceDate组合成唯一标识(因为要同时匹配这两个字段才合并)。如果你用的是Java 16+,可以直接用record(自动生成equals和hashCode,省代码);如果是低版本,就自定义一个不可变类:
分组键定义(Java 16+)
// 记录类自动实现equals和hashCode,完美作为Map的键 record AccountKey(String accountId, Date invoiceDate) {}
分组键定义(Java 8-15)
import java.util.Date; import java.util.Objects; class AccountKey { private final String accountId; private final Date invoiceDate; public AccountKey(String accountId, Date invoiceDate) { this.accountId = accountId; this.invoiceDate = invoiceDate; } // 必须重写equals和hashCode,否则HashMap无法正确分组 @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; AccountKey that = (AccountKey) o; return Objects.equals(accountId, that.accountId) && Objects.equals(invoiceDate, that.invoiceDate); } @Override public int hashCode() { return Objects.hash(accountId, invoiceDate); } }
Stream合并代码
import java.util.List; import java.util.Objects; import java.util.stream.Collectors; // 假设你的原列表是List<AccountInfo> accountInfoLst; List<AccountInfo> mergedList = accountInfoLst.stream() // 按AccountKey分组 .collect(Collectors.groupingBy( info -> new AccountKey(info.getAccountId(), info.getInvoiceDate()), // 归约每个分组的元素,合并vat和totalFee Collectors.reducing( null, // 初始值 (acc, current) -> { if (acc == null) { // 第一个元素,创建新对象避免修改原列表的原始数据 AccountInfo newInfo = new AccountInfo(); newInfo.setAccountId(current.getAccountId()); newInfo.setInvoiceDate(current.getInvoiceDate()); newInfo.setVat(current.getVat()); newInfo.setTotalFee(current.getTotalFee()); return newInfo; } // 累加字段,注意处理null值(如果字段可能为空的话) acc.setVat((acc.getVat() == null ? 0 : acc.getVat()) + (current.getVat() == null ? 0 : current.getVat())); acc.setTotalFee((acc.getTotalFee() == null ? 0 : acc.getTotalFee()) + (current.getTotalFee() == null ? 0 : current.getTotalFee())); return acc; } ) )) .values() // 取出所有合并后的AccountInfo .stream() .filter(Objects::nonNull) // 过滤空值(如果原列表为空的话) .collect(Collectors.toList());
方案二:传统循环+HashMap 实现(直观易调试)
如果团队里有人对Stream不太熟悉,或者需要更直观的调试流程,用传统循环+HashMap的方式更稳妥:
import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; List<AccountInfo> mergedList = new ArrayList<>(); Map<AccountKey, AccountInfo> mergeMap = new HashMap<>(); for (AccountInfo current : accountInfoLst) { AccountKey key = new AccountKey(current.getAccountId(), current.getInvoiceDate()); AccountInfo existing = mergeMap.get(key); if (existing == null) { // 分组里没有这个键,创建新对象存入Map AccountInfo newInfo = new AccountInfo(); newInfo.setAccountId(current.getAccountId()); newInfo.setInvoiceDate(current.getInvoiceDate()); newInfo.setVat(current.getVat()); newInfo.setTotalFee(current.getTotalFee()); mergeMap.put(key, newInfo); } else { // 已经存在,累加字段 existing.setVat((existing.getVat() == null ? 0 : existing.getVat()) + (current.getVat() == null ? 0 : current.getVat())); existing.setTotalFee((existing.getTotalFee() == null ? 0 : existing.getTotalFee()) + (current.getTotalFee() == null ? 0 : current.getTotalFee())); } } // 把Map里的合并结果转成List mergedList.addAll(mergeMap.values());
关键注意点
- Date的匹配精度:默认
Date.equals()是精确到毫秒的,如果你的需求是“同一天就算匹配”(不管时分秒),需要先把invoiceDate截断到日期部分,比如:
然后用// 用Apache Commons Lang的工具类(推荐) Date truncatedDate = org.apache.commons.lang3.time.DateUtils.truncate(current.getInvoiceDate(), java.util.Calendar.DATE); // 或者自己手动截断 java.util.Calendar cal = java.util.Calendar.getInstance(); cal.setTime(current.getInvoiceDate()); cal.set(java.util.Calendar.HOUR_OF_DAY, 0); cal.set(java.util.Calendar.MINUTE, 0); cal.set(java.util.Calendar.SECOND, 0); cal.set(java.util.Calendar.MILLISECOND, 0); Date truncatedDate = cal.getTime();truncatedDate作为AccountKey的一部分。 - 空值处理:如果
vat或totalFee可能为null,一定要在累加时做null判断,否则会抛出NullPointerException。 - 避免修改原列表:上面的代码都是创建新的
AccountInfo对象,而不是直接修改原列表里的元素,这样不会影响原始数据,更安全。
内容的提问来源于stack exchange,提问作者Aditya Thakur
相关产品推荐
相关产品推荐

