【问题标题】:Java 8 Stream Merge Partial DuplicatesJava 8 流合并部分重复
【发布时间】:2018-01-24 02:09:53
【问题描述】:

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

【问题讨论】:

    标签: java-8 java-stream


    【解决方案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 构建的,如果键不存在,它将添加指定的对象,如果键已经存在,则评估合并函数。添加联系人后,merge 函数返回旧对象,因此我们可以进行参考比较 (a != …) 以确定我们有应删除的重复项。

    【讨论】:

      【解决方案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());
      

      【讨论】:

      • 我已经接受了这个答案,因为它使用的是流API,我不需要修改POJO。
      【解决方案3】:

      您可以将两个构造函数和一个 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进行分组,并通过Account.merge的方法合并具有相等accountId的账户的联系人。最后,获取地图的值:

      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&lt;Account&gt;。如果你需要List&lt;Account&gt;,你可以这样做:

      List<Account> list = new ArrayList<>(result);
      

      【讨论】:

      • @Holger 我已经编辑了我的答案。我已经删除了自定义收集器示例,因为没有在 merge 方法或新的专用 accumulate 方法中设置属性的情况下创建具有 accountIdAccount 的简洁方法。再次感谢您的反馈。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-06-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-10-21
      相关资源
      最近更新 更多