Java 8 Stream Merge Partial Duplicates

时间:2018-01-24 02:09:54

标签: java-8 java-stream

我有一个看起来像这样的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"}         |
+-----------+------------------+

3 个答案:

答案 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 != …)以确定我们有一个应该删除的副本。