Совокупный список‹X› в список‹X› с Java 8 Stream API

У меня есть следующий класс:

class Money {
  CurrencyUnit currencyUnit;
  BigDecimal amount;
}

В моем приложении я получаю случайный список Money объектов:

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

Теперь я хотел бы использовать Java 8 Stream API для создания следующего результата (просто вызывая BigDecimal::add для каждой суммы currencyUnit):

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

Что я уже знаю/делал:

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

И вот оно уже заканчивается. Я знаю, что могу использовать Collector для создания Map<CurrencyUnit, List<Money>>:

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

Но тогда мне все равно придется пройтись по всем парам ключ-значение и просуммировать данные суммы.

Как правильно (и, возможно, проще всего) это сделать? Это не может быть так сложно, верно? :)


РЕДАКТИРОВАТЬ: Если не совсем понятно, что мне нужно, вот мой старый Java-код:

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

РЕДАКТИРОВАТЬ 2: Другое решение, которое не очень элегантно:

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

person Benjamin M    schedule 04.07.2014    source источник


Ответы (1)


Вы можете использовать сборщик groupingBy для группировки объектов по CurrencyUnit. Без второго аргумента метод groupingBy собирает элементы в список. Однако вы также можете указать нисходящий сборщик, если вам нужно что-то еще.

Вы можете использовать Collectors::summingInt и Collectors::summingLong вместо int и long. Для BigDecimal вы можете вернуться к 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)));

Изменить. Вы также можете создать 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());
person nosid    schedule 04.07.2014
comment
Спасибо. Это близко к тому, что мне нужно, но ваш код дает мне Map. Нельзя ли вернуть List или Set? (без создания вручную новых Money объектов и т. д.) - person Benjamin M; 04.07.2014
comment
Буквально 5 минут назад мне пришла в голову идея с перебором entrySet, но я подумал: Это действительно некрасиво, должно же быть что-то более сексуальное/элегантное. --- Но я думаю, что ваш ответ - единственный способ сделать это. --- прямо сейчас я подумал о: moneyStream.collect(Collectors.groupingBy(m -> m.getCurrencyUnit()); а затем использовать сгенерированный Map<CurrencyUnit, List<Money>> с map.values().stream() для создания List<Money> ?! - person Benjamin M; 04.07.2014