How avoid duplicates in result from stream?

Viewed 134

CashBox class:

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

    // all-args constructor
}

Merchant class:

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

   // all-args constructor
}

Input data:

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)); 

My task

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

I'm trying to solve this task using Stream API. And wrote the following code:

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());

Result

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

But obviously, the stream returns four objects instead of the needed two. I realize, that the map(2nd row) creates four objects for each cashBoxId. And I don't get how to filter by merchantId or get results without duplicates.

2 Answers

One way is to use groupingBy. Group by the merchant Ids, then for each group, map to the total amount, and reduce, using the corresponding collectors. This will get you a Map<Long, BigDecimal> containing merchant Ids and total amounts. You can then map each entry of this map to a merchant:

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());

There are

Here is a "one-liner" using a single Stream. The idea is to use the advantage of the grouping using Stream::toMap, mapping value using Function<CashBox, Merchant> and merging them into a single object using 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();
Related