我有一个看起来像这样的POJO:
public class Account {
private Integer accountId;
private List<String> contacts;
}
equals和hashCode方法设置为使用accountId
字段来标识唯一性,因此任何具有相同accountId
的帐户都是相同的,无论contacts
包含什么。
我有一个帐户列表,并且有一些重复项具有相同的accountId
。如何使用Java 8 Stream API将这些重复项合并在一起?
例如,帐户列表包含:
+-----------+----------+
| accountId | contacts |
+-----------+----------+
| 1 | {"John"} |
| 1 | {"Fred"} |
| 2 | {"Mary"} |
+-----------+----------+
我希望它能够生成这样的帐户列表:
+-----------+------------------+
| accountId | contacts |
+-----------+------------------+
| 1 | {"John", "Fred"} |
| 2 | {"Mary"} |
+-----------+------------------+
答案 0 :(得分:2)
使用Collectors.toMap
参考:https://docs.oracle.com/javase/8/docs/api/java/util/stream/Collectors.html#toMap-java.util.function.Function-java.util.function.Function-java.util.function.BinaryOperator-
@lombok.Value
class Account {
Integer accountId;
List<String> contacts;
}
List<Account> accounts = new ArrayList<>();
//Fill
List<Account> result = new ArrayList<>(accounts.stream()
.collect(
Collectors.toMap(Account::getAccountId, Function.identity(), (Account account1, Account account2) -> {
account1.getContacts().addAll(account2.getContacts());
account2.getContacts().clear();
return account1;
})
)
.values());
答案 1 :(得分:1)
您可以向merge
类添加两个构造函数和Account
方法,以组合联系人:
public class Account {
private final Integer accountId;
private List<String> contacts = new ArrayList<>();
public Account(Integer accountId) {
this.accountId = accountId;
}
// Copy constructor
public Account(Account another) {
this.accountId = another.accountId;
this.contacts = new ArrayList<>(another.contacts);
}
public Account merge(Account another) {
this.contacts.addAll(another.contacts);
return this;
}
// TODO getters and setters
}
然后,你有几个选择。一种方法是使用Collectors.toMap
向地图收集帐户,按accountId
分组,并通过accountId
方法将帐户的联系人合并为Account.merge
。最后,获取地图的值:
Collection<Account> result = accounts.stream()
.collect(Collectors.toMap(
Account::getAccountId, // group by accountId (keys)
Account::new, // use copy constructor (values)
Account::merge)) // merge values with equal key
.values();
您需要对值使用复制构造函数,否则在调用Account.merge
时,您将改变原始列表的帐户。
等效方式(没有流)将使用Map.merge
方法:
Map<Integer, Account> map = new HashMap<>();
accounts.forEach(a ->
map.merge(a.getAccountId(), new Account(a), Account::merge));
Collection<Account> result = map.values();
同样,您需要使用复制构造函数来避免原始列表帐户上的意外突变。
更优化的第三种替代方案(因为它不为列表的每个元素创建新帐户)包括使用Map.computeIfAbsent
方法:
Map<Integer, Account> map = new HashMap<>();
accounts.forEach(a -> map.computeIfAbsent(
a.getAccountId(), // group by accountId (keys)
Account::new) // invoke new Account(accountId) if absent
.merge(a)); // merge account's contacts
Collection<Account> result = map.values();
上述所有替代方案均返回Collection<Account>
。如果您需要List<Account>
,则可以执行以下操作:
List<Account> list = new ArrayList<>(result);
答案 2 :(得分:1)
一个干净的Stream API解决方案可能很安静,所以也许你最好使用一个服从限制较少的Collection API解决方案。
HashMap<Integer, Account> tmp = new HashMap<>();
listOfAccounts.removeIf(a -> a != tmp.merge(a.getAccountId(), a, (o,n) -> {
o.getContacts().addAll(n.getContacts());
return o;
}));
在将联系人添加到该ID的第一个帐户后,这会直接删除列表中具有重复ID的所有元素。
当然,这假设列表支持删除,getContacts()
返回的列表是对存储列表的引用,并支持添加元素。
解决方案围绕Map.merge
构建,如果密钥不存在,将添加指定的对象;如果密钥已存在,则评估合并函数。合并函数在添加联系人后返回旧对象,因此我们可以进行参考比较(a != …
)以确定我们有一个应该删除的副本。