Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Aggregate List<X> to List<X> with Java 8 Stream API

I've got the following class:

class Money {
  CurrencyUnit currencyUnit;
  BigDecimal amount;
}

In my application, I get some random list of Money objects:

currencyUnit | amount
---------------------
EUR          | 5.1
EUR          | 0
USD          | 1.09
EUR          | 42
USD          | 3

Now I'd like to use the Java 8 Stream API to create the following result (simply calling BigDecimal::add for each currencyUnit's amount):

currencyUnit | amount
---------------------
EUR          | 47.1
USD          | 4.09

What I already know/did:

Stream<Money> moneyStream = moneyList.stream();

And here it ends already. I know I can use a Collector to produce a Map<CurrencyUnit, List<Money>>:

moneyStream.collect(Collectors.groupingBy(m -> m.getCurrencyUnit());

But then I still have to go through all key-value-pairs and sum up the amount data.

What's the right (and possibly easiest way) to do that? It can't be that complicated, right? :)


EDIT: If it's not that clear what I need, here's my old-way Java code:

Map<CurrencyUnit, Money> map = new HashMap<>();
moneyList.stream().forEach(e -> {
    Money m = map.get(e.getCurrencyUnit());
    if(m == null) {
        m = new Money();
        m.setAmount(BigDecimal.ZERO);
        m.setCurrencyUnit(e.getCurrencyUnit());
        map.put(e.getCurrencyUnit(), m);
    }
    m.setAmount(m.getAmount().add(e.getAmount()));
});
return map.values();

EDIT 2: Another solution, which isn't really elegant:

List<Money> list = inputList.stream()
    .collect(Collectors.groupingBy(Money::getCurrencyUnit))
    .values().stream().map(ml -> {
        Money money = new Money();
        ml.forEach(m -> {
            if(money.getCurrencyUnit() == null) {
                money.setCurrencyUnit(m.getCurrencyUnit());
                money.setAmount(m.getAmount());
            } else {
                money.setAmount(money.getAmount().add(m.getAmount()));
            }
        });
        return money;
    }).collect(Collectors.toList());
like image 217
Benjamin M Avatar asked Jul 04 '14 15:07

Benjamin M


People also ask

What are the aggregate operations in Java 8 streams?

Aggregate operations − Stream supports aggregate operations like filter, map, limit, reduce, find, match, and so on. Pipelining − Most of the stream operations return stream itself so that their result can be pipelined.

Can we convert Stream to List in Java?

Using Collectors. Get the Stream to be converted. Collect the stream as List using collect() and Collectors. toList() methods. Convert this List into an ArrayList.

How do you collect a Stream from an ArrayList?

All you need to do is first get the stream from List by calling stream() method, then call the filter() method to create a new Stream of filtered values and finally call the Collectors. toCollection(ArrayList::new) to collect those elements into an ArrayList.


1 Answers

You can use a groupingBy collector to group the objects by CurrencyUnit. Without second argument, the groupingBy method collects the elements into a list. However, you can also specify a downstream collector if you need something else.

You can use Collectors::summingInt and Collectors::summingLong for int and long. For BigDecimal, you can fall back to Collectors::reducing:

import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.reducing;

Map<CurrencyUnit, BigDecimal> result = moneyList.stream()
    .collect(
        groupingBy(
            Money::getCurrencyUnit,
            reducing(BigDecimal.ZERO, Money::getAmount, BigDecimal::add)));

Edit: You can also create List<Money>:

List<Money> result = moneyList.stream()
    .collect(
        groupingBy(
            Money::getCurrencyUnit,
            reducing(BigDecimal.ZERO, Money::getAmount, BigDecimal::add)))
    .entrySet().stream()
    .map(e -> new Money(e.getKey(), e.getValue())
    .collect(toList());
like image 190
nosid Avatar answered Oct 19 '22 23:10

nosid