vkt
vkt

Reputation: 141

Java 8 lambda sum, count and group by

Select sum(paidAmount), count(paidAmount), classificationName,
From tableA
Group by classificationName;

How can i do this in Java 8 using streams and collectors?

Java8:

lineItemList.stream()
            .collect(Collectors.groupingBy(Bucket::getBucketName,
                       Collectors.reducing(BigDecimal.ZERO,
                                           Bucket::getPaidAmount,
                                           BigDecimal::add)))

This gives me sum and group by. But how can I also get count on the group name ?

Expectation is :

100, 2, classname1 
50, 1, classname2
150, 3, classname3

Upvotes: 6

Views: 2611

Answers (3)

fps
fps

Reputation: 34480

As you're using BigDecimal for the amounts (which is the correct approach, IMO), you can't make use of Collectors.summarizingDouble, which summarizes count, sum, average, min and max in one pass.

Alexis C. has already shown in his answer one way to do it with streams. Another way would be to write your own collector, as shown in Holger's answer.

Here I'll show another way. First let's create a container class with a helper method. Then, instead of using streams, I'll use common Map operations.

class Statistics {
    int count;
    BigDecimal sum;

    Statistics(Bucket bucket) {
        count = 1;
        sum = bucket.getPaidAmount();
    }

    Statistics merge(Statistics another) {
        count += another.count;
        sum = sum.add(another.sum);
        return this;
    }
}

Now, you can make the grouping as follows:

Map<String, Statistics> result = new HashMap<>();
lineItemList.forEach(b -> 
    result.merge(b.getBucketName(), new Statistics(b), Statistics::merge));

This works by using the Map.merge method, whose docs say:

If the specified key is not already associated with a value or is associated with null, associates it with the given non-null value. Otherwise, replaces the associated value with the results of the given remapping function

Upvotes: 5

Holger
Holger

Reputation: 298579

Using an extended version of the Statistics class of this answer,

class Statistics {
    int count;
    BigDecimal sum;

    Statistics(Bucket bucket) {
        count = 1;
        sum = bucket.getPaidAmount();
    }
    Statistics() {
        count = 0;
        sum = BigDecimal.ZERO;
    }

    void add(Bucket b) {
        count++;
        sum = sum.add(b.getPaidAmount());
    }

    Statistics merge(Statistics another) {
        count += another.count;
        sum = sum.add(another.sum);
        return this;
    }
}

you can use it in a Stream operation like

Map<String, Statistics> map = lineItemList.stream()
    .collect(Collectors.groupingBy(Bucket::getBucketName,
        Collector.of(Statistics::new, Statistics::add, Statistics::merge)));

this may have a small performance advantage, as it only creates one Statistics instance per group for a sequential evaluation. It even supports parallel evaluation, but you’d need a very large list with sufficiently large groups to get a benefit from parallel evaluation.

For a sequential evaluation, the operation is equivalent to

lineItemList.forEach(b ->
    map.computeIfAbsent(b.getBucketName(), x -> new Statistics()).add(b));

whereas merging partial results after a parallel evaluation works closer to the example already given in the linked answer, i.e.

secondMap.forEach((key, value) -> firstMap.merge(key, value, Statistics::merge));

Upvotes: 6

Alexis C.
Alexis C.

Reputation: 93902

You could reduce pairs where the keys would hold the sum and the values would hold the count:

Map<String, SimpleEntry<BigDecimal, Long>> map = 
    lineItemList.stream()
                .collect(groupingBy(Bucket::getBucketName,
                         reducing(new SimpleEntry<>(BigDecimal.ZERO, 0L), 
                                  b -> new SimpleEntry<>(b.getPaidAmount(), 1L), 
                                  (v1, v2) -> new SimpleEntry<>(v1.getKey().add(v2.getKey()), v1.getValue() + v2.getValue()))));

although Collectors.toMap looks cleaner:

Map<String, SimpleEntry<BigDecimal, Long>> map = 
    lineItemList.stream()
                .collect(toMap(Bucket::getBucketName,
                               b -> new SimpleEntry<>(b.getPaidAmount(), 1L),
                               (v1, v2) -> new SimpleEntry<>(v1.getKey().add(v2.getKey()), v1.getValue() + v2.getValue())));

Upvotes: 3

Related Questions