Java поток группирует и суммирует несколько полей - PullRequest
0 голосов
/ 28 августа 2018

У меня есть список fooList

class Foo {
    private String category;
    private int amount;
    private int price;

    ... constructor, getters & setters
}

Я бы хотел сгруппировать по категориям, а затем суммировать сумму и цену.

Результат будет сохранен на карте:

Map<Foo, List<Foo>> map = new HashMap<>();

Ключом является Foo, содержащая итоговую сумму и цену со списком в качестве значения для всех объектов той же категории.

Пока я пробовал следующее:

Map<String, List<Foo>> map = fooList.stream().collect(groupingBy(Foo::getCategory()));

Теперь мне нужно только заменить ключ String на объект Foo, содержащий итоговую сумму и цену. Вот где я застрял. Кажется, я не могу найти способ сделать это.

Ответы [ 5 ]

0 голосов
/ 28 августа 2018

Если у вас есть специальный выделенный конструктор и методы hashCode и equals, последовательно реализованные в Foo следующим образом:

public Foo(Foo that) { // not a copy constructor!!!
    this.category = that.category;
    this.amount = 0;
    this.price = 0;
}

public int hashCode() {
    return Objects.hashCode(category);
}

public boolean equals(Object another) {
   if (another == this) return true;
   if (!(another instanceof Foo)) return false;
   Foo that = (Foo) another;
   return Objects.equals(this.category, that.category);
}

Приведенные выше реализации hashCode и equals позволяют использовать Foo в качестве значимого ключа на карте (в противном случае ваша карта будет повреждена).

Теперь, с помощью нового метода в Foo, который выполняет агрегацию атрибутов amount и price одновременно, вы можете делать то, что вы хотите, в 2 этапа. Сначала метод:

public void aggregate(Foo that) {
    this.amount += that.amount;
    this.price += that.price;
}

Теперь окончательное решение:

Map<Foo, List<Foo>> result = fooList.stream().collect(
    Collectors.collectingAndThen(
        Collectors.groupingBy(Foo::new), // works: special ctor, hashCode & equals
        m -> { m.forEach((k, v) -> v.forEach(k::aggregate)); return m; }));

РЕДАКТИРОВАТЬ: несколько замечаний отсутствовали ...

С одной стороны, это решение заставляет вас использовать реализацию hashCode и equals, которая считает два разных экземпляра Foo равными, если они принадлежат одному и тому же category. Возможно, это нежелательно, или у вас уже есть реализация, которая учитывает больше или другие атрибуты.

С другой стороны, использование Foo в качестве ключа карты, которая используется для группировки экземпляров по одному из ее атрибутов, является довольно редким случаем использования. Я думаю, что было бы лучше просто использовать атрибут category для группировки по категориям и иметь две карты: Map<String, List<Foo>> для сохранения групп и Map<String, Foo> для хранения агрегированных price и amount, причем ключ category в обоих случаях.

Кроме того, это решение изменяет ключи карты после того, как в нее введены записи. Это опасно, потому что это может сломать карту. Однако здесь я изменяю только атрибуты Foo, которые не участвуют ни в реализации hashCode, ни equals Foo. Я думаю, что этот риск приемлем в этом случае из-за необычности требования.

0 голосов
/ 28 августа 2018

Мой вариант Ответ Sweepers использует уменьшающий коллектор вместо потоковой передачи дважды для суммирования отдельных полей:

        Map<Foo, List<Foo>> map = fooList.stream()
                .collect(Collectors.groupingBy(Foo::getCategory))
                .entrySet().stream()
                .collect(Collectors.toMap(e -> e.getValue().stream().collect(
                            Collectors.reducing((l, r) -> new Foo(l.getCategory(),
                                        l.getAmount() + r.getAmount(),
                                        l.getPrice() + r.getPrice())))
                            .get(), 
                            e -> e.getValue()));

Это не совсем лучше , хотя, поскольку это создает много недолговечных Foos.

Обратите внимание, однако, что Foo требуется для обеспечения hashCode - и equals - реализаций, которые учитывают только category, чтобы результирующий map работал правильно. Это, вероятно, не то, что вы хотите для Foo в целом. Я бы предпочел определить отдельный класс FooSummary для хранения агрегированных данных.

0 голосов
/ 28 августа 2018

Мой взгляд на решение:)

public static void main(String[] args) {
    List<Foo> foos = new ArrayList<>();
    foos.add(new Foo("A", 1, 10));
    foos.add(new Foo("A", 2, 10));
    foos.add(new Foo("A", 3, 10));
    foos.add(new Foo("B", 1, 10));
    foos.add(new Foo("C", 1, 10));
    foos.add(new Foo("C", 5, 10));

    List<Foo> summarized = new ArrayList<>();
    Map<Foo, List<Foo>> collect = foos.stream().collect(Collectors.groupingBy(new Function<Foo, Foo>() {
        @Override
        public Foo apply(Foo t) {
            Optional<Foo> fOpt = summarized.stream().filter(e -> e.getCategory().equals(t.getCategory())).findFirst();
            Foo f;
            if (!fOpt.isPresent()) {
                f = new Foo(t.getCategory(), 0, 0);
                summarized.add(f);
            } else {
                f = fOpt.get();
            }
            f.setAmount(f.getAmount() + t.getAmount());
            f.setPrice(f.getPrice() + t.getPrice());
            return f;
        }
    }));
    System.out.println(collect);
}
0 голосов
/ 28 августа 2018

Немного некрасиво, но должно работать:

list.stream().collect(Collectors.groupingBy(Foo::getCategory))
    .entrySet().stream()
    .collect(Collectors.toMap(x -> {
        int sumAmount = x.getValue().stream().mapToInt(Foo::getAmount).sum();
        int sumPrice= x.getValue().stream().mapToInt(Foo::getPrice).sum();
        return new Foo(x.getKey(), sumAmount, sumPrice);
    }, Map.Entry::getValue));
0 голосов
/ 28 августа 2018

Я предлагаю вам создать вспомогательный класс, который будет содержать сумму и цену

final class Pair {
    final int amount;
    final int price;

    Pair(int amount, int price) {
        this.amount = amount;
        this.price = price;
    }
}

А затем просто собрать список на карту:

List<Foo> list =//....;

Map<Foo, Pair> categotyPrise = list.stream().collect(Collectors.toMap(foo -> foo,
                    foo -> new Pair(foo.getAmount(), foo.getPrice()),
                    (o, n) -> new Pair(o.amount + n.amount, o.price + n.price)));
...