如何避免流的结果重复?

How avoid duplicates in result from stream?

钱箱class:

public class CashBox {
    private long cashBoxId;
    private BigDecimal totalAmount;
    private long merchantId;

    // all-args constructor
}

商家class:

public class Merchant {
    private long merchantId;
    private BigDecimal totalAmount;

   // all-args constructor
}

输入数据:

List<CashBox> cashBoxes = List.of(
    new CashBox(1, new BigDecimal(1000), 1),
    new CashBox(2, new BigDecimal(2000), 1),
    new CashBox(3, new BigDecimal(3000), 2),
    new CashBox(4, new BigDecimal(500), 2)); 

我的任务

Calculate the total amount for each merchant and return the merchant list

我正在尝试使用 Stream API 解决此任务。并编写了以下代码:

List<Merchant> merchant =  cashBoxes.stream()
    .map(merch -> new Merchant(
        merch.getMerchantId(), 
        cashBoxes.stream()
                 .filter(cashBox -> cashBox.getMerchantId() == merch.getMerchantId())
                 .map(CashBox::getTotalAmount)
                 .reduce(BigDecimal.ZERO, BigDecimal::add)))
    .collect(Collectors.toList());

结果

[Merchant{merchantId=1, totalAmount=3000}, Merchant{merchantId=1, totalAmount=3000}, Merchant{merchantId=2, totalAmount=3500}, Merchant{merchantId=2, totalAmount=3500}]

但显然,流 returns 四个对象而不是所需的两个。 我意识到,地图(第二行)为每个 cashBoxId 创建了四个对象。而且我不知道如何按 merchantId 过滤或获得没有重复的结果。

一种方法是使用 groupingBy。按商户 ID 分组,然后为每个组映射到总量,并使用相应的收集器进行归约。这将为您提供包含商家 ID 和总金额的 Map<Long, BigDecimal>。然后您可以将此地图的每个条目映射到商家:

cashBoxes.stream().collect(Collectors.groupingBy(
    CashBox::getMerchantId, // group by merchant Id
    // for each group...
    Collectors.mapping(// map to total amount
        CashBox::getTotalAmount,
        Collectors.reducing(BigDecimal.ZERO, BigDecimal::add) // sum
    )
)).entrySet().stream()
    .map(x -> new Merchant(x.getKey(), x.getValue())) // map to merchant
    .collect(Collectors.toList());

这里是使用单个 Stream 的“单行”。这个想法是利用使用 Stream::toMap 分组的优势,使用 Function<CashBox, Merchant> 映射值并使用 BinaryOperator<Merchant>:

将它们合并到一个对象中
Collection<Merchant> merchants = cashBoxes.stream()
    .collect(Collectors.toMap(
        CashBox::getMerchantId,
        cashBox -> new Merchant(cashBox.getMerchantId(), cashBox.getTotalAmount()),
        (l, r) -> {
            l.setTotalAmount(l.getTotalAmount().add(r.getTotalAmount()));
            return l;
        }
    ))
    .values();