繁体   English   中英

如何在 java 流中合并两个 groupingBy?

[英]How to merge two groupingBy in java streams?

我有一个输入 object

  @Getter
  class Txn {

    private String hash;
    private String withdrawId;
    private String depositId;
    private Integer amount;
    private String date;

  }

output object 是

  @Builder
  @Getter
  class UserTxn {

    private String hash;
    private String walletId;
    private String txnType;
    private Integer amount;
  }

Txn object 中从withdrawId -> depositId转移金额

我正在做的是所有交易(Txn 对象)添加到按 hash 分组的单个金额中。

但是为此,我必须创建两个流用于通过 withdrawId 进行分组,第二个或用于 depositId,然后是第三个 stream 用于合并它们

按 withdrawId 分组

var withdrawStream = txnList.stream().collect(Collectors.groupingBy(Txn::getHash, LinkedHashMap::new,
        Collectors.groupingBy(Txn::getWithdrawId, LinkedHashMap::new, Collectors.toList())))
    .entrySet().stream().flatMap(hashEntrySet -> hashEntrySet.getValue().entrySet().stream()
        .map(withdrawEntrySet ->
            UserTxn.builder()
                .hash(hashEntrySet.getKey())
                .walletId(withdrawEntrySet.getKey())
                .txnType("WITHDRAW")
                .amount(withdrawEntrySet.getValue().stream().map(Txn::getAmount).reduce(0, Integer::sum))
                .build()
        ));

按 depositId 分组

var depositStream = txnList.stream().collect(Collectors.groupingBy(Txn::getHash, LinkedHashMap::new,
        Collectors.groupingBy(Txn::getDepositId, LinkedHashMap::new, Collectors.toList())))
    .entrySet().stream().flatMap(hashEntrySet -> hashEntrySet.getValue().entrySet().stream()
        .map(withdrawEntrySet ->
            UserTxn.builder()
                .hash(hashEntrySet.getKey())
                .walletId(withdrawEntrySet.getKey())
                .txnType("DEPOSIT")
                .amount(withdrawEntrySet.getValue().stream().map(Txn::getAmount).reduce(0, Integer::sum))
                .build()
        ));

然后再次合并它们,使用存款 - 取款

var res = Stream.concat(withdrawStream, depositStream).collect(Collectors.groupingBy(UserTxn::getHash, LinkedHashMap::new,
    Collectors.groupingBy(UserTxn::getWalletId, LinkedHashMap::new, Collectors.toList())))
    .entrySet().stream().flatMap(hashEntrySet -> hashEntrySet.getValue().entrySet().stream()
        .map(withdrawEntrySet -> {
              var depositAmount = withdrawEntrySet.getValue().stream().filter(userTxn -> userTxn.txnType.equals("DEPOSIT")).map(UserTxn::getAmount).reduce(0, Integer::sum);
              var withdrawAmount = withdrawEntrySet.getValue().stream().filter(userTxn -> userTxn.txnType.equals("WITHDRAW")).map(UserTxn::getAmount).reduce(0, Integer::sum);
              var totalAmount = depositAmount-withdrawAmount;
              return UserTxn.builder()
                  .hash(hashEntrySet.getKey())
                  .walletId(withdrawEntrySet.getKey())
                  .txnType(totalAmount > 0 ? "DEPOSIT": "WITHDRAW")
                  .amount(totalAmount)
                  .build();
            }
        ));

我的问题是,如何在一个 stream 中执行此操作。就像以某种方式分组一样,withdrawId 和 depositId 是一个分组。

就像是

res = txnList.stream()
        .collect(Collectors.groupingBy(Txn::getHash,
            LinkedHashMap::new,
            Collectors.groupingBy(Txn::getWithdrawId && Txn::getDepositId,
                LinkedHashMap::new, Collectors.toList())))
        .entrySet().stream().flatMap(hashEntrySet -> hashEntrySet.getValue().entrySet().stream()
            .map(walletEntrySet ->
                {
                  var totalAmount = walletEntrySet.getValue().stream().map(
                      txn -> Objects.equals(txn.getDepositId(), walletEntrySet.getKey())
                          ? txn.getAmount() : (-txn.getAmount())).reduce(0, Integer::sum);
                  return UserTxn.builder()
                      .hash(hashEntrySet.getKey())
                      .walletId(walletEntrySet.getKey())
                      .txnType("WITHDRAW")
                      .amount(totalAmount)
                      .build();
                }
            ));

我不会在我的代码中使用它,因为我认为它不可读并且将来很难更改和管理(SOLID)。 但如果你仍然想要这个 -

如果我的设计正确,每个用户 hash 是唯一的,交易将只有存款或取款,如果是这样,这将起作用-

您可以像在示例中那样通过收集器链接将 groupBy 三倍。 您可以通过简单的 map function 创建Txn类型,只需检查哪个 ID 是 null。

Map<String, Map<String, Map<String, List<Txn>>>> groupBy = 
      txnList.stream()
             .collect(Collectors.groupingBy(Txn::getHash, LinkedHashMap::new,
                       Collectors.groupingBy(Txn::getDepositId, LinkedHashMap::new,
                         Collectors.groupingBy(Txn::getWithdrawId, LinkedHashMap::new, Collectors.toList()))));

然后在这个 stream 上使用您示例中的逻辑。

长话短说

对于那些没有理解问题的人,OP 想要从每个Txn实例( Txn可能代表交易)生成两个数据和平: hashwithdrawId + 聚合金额,以及hashdepositId + 聚合金额。

然后他们想将这两个部分合并在一起(因此他们创建了两个流,然后将它们连接起来)。

注意:原始代码中似乎有一个逻辑流程:与withdrawIddepositId相关联的金额相同。 这并不反映该金额已从一个帐户中提取并转移到另一个帐户。 因此,如果depositId金额按原样使用,而withdrawId - 否定(即-1 * amount ),这将是有意义的。

Collectors.teeing()

您可以使用 Java 12 Collector teeing teeing()并将 stream 元素内部分组为两个不同的地图:

  • 第一个通过withdrawIdhash对 stream 数据进行分组。

  • 另一个通过对数据depositIdhash进行分组。

Teeing 期望三个 arguments:2 个下游收集器和一个 Function 组合收集器产生的结果。

作为 teeing( teeing()的下游,我们可以使用 Collectors groupingBy()summingInt()的组合,第二个需要累积 integer 笔交易amount

请注意,没有必要使用嵌套的 Collector groupingBy()相反,我们可以创建一个自定义类型来保存idhash并且它的equals/hashCode应该基于包装的 id 和 hash 来实现)。 Java 16 条record非常适合这个角色:

public record HashWalletId(String hash, String walletId) {}

HashWalletId的实例将用作两个中间映射中的密钥。

teeing teeing()终结者 function会将两个 Maps 的结果合并在一起。

唯一剩下的就是从 map 个条目中生成UserTxn的实例。

List<Txn> txnList = // initializing the list
        
List<UserTxn> result = txnList.stream()
    .collect(Collectors.teeing(
        Collectors.groupingBy(
            txn -> new HashWalletId(txn.getHash(), txn.getWithdrawId()),
            Collectors.summingInt(txn -> -1 * txn.getAmount())), // because amount has been withdrawn
        Collectors.groupingBy(
            txn -> new HashWalletId(txn.getHash(), txn.getDepositId()),
            Collectors.summingInt(Txn::getAmount)),
        (map1, map2) -> {
            map2.forEach((k, v) -> map1.merge(k, v, Integer::sum));
            return map1;
        }
    ))
    .entrySet().stream()
    .map(entry -> UserTxn.builder()
        .hash(entry.getKey().hash())
        .walletId(entry.getKey().walletId())
        .txnType(entry.getValue() > 0 ? "DEPOSIT" : "WITHDRAW")
        .amount(entry.getValue())
        .build()
    )
    .toList(); // remove the terminal operation if your goal is to produce a Stream

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM