1

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.

Sotirios Delimanolis
  • 274,122
  • 60
  • 696
  • 724
Roman Matviichuk
  • 141
  • 1
  • 1
  • 7

2 Answers2

2

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

Sweeper
  • 213,210
  • 22
  • 193
  • 313
2

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();
Nikolas Charalambidis
  • 40,893
  • 16
  • 117
  • 183
  • 1. why would you need an `if..else`, are you not already reducing the list of cashboxes all of which have the same `merchantId`? 2. Use of [`toMap` would have simplified](https://stackoverflow.com/questions/57041896/java-streams-replacing-groupingby-and-reducing-by-tomap) – Naman Jun 13 '21 at 14:57
  • @Naman: I missed that! Thanks for the note! – Nikolas Charalambidis Jun 13 '21 at 15:06