Search code examples
javajava-streamcollectors

Dividing the sum of two different properties in Java Streams


I have a stream of objects. I want to group them and calculate the division of the sum of values I get from object functions.

import java.util.Map;
import java.util.stream.Stream;

record TestRecord(String type, int a, int b) {
    public int getA() { return a; }
    public int getB() { return b; }
}
    
public class Test {    
    public static void main(String[] args) {
        Stream<TestRecord> testRecords = Stream.of(
            new TestRecord("TYPE1", 1, 2),
            new TestRecord("TYPE1", 3, 4),
            new TestRecord("TYPE2", 5, 6),
            new TestRecord("TYPE2", 7, 8),
            new TestRecord("TYPE2", 9, 10)
        );
    }
    
    //It should return {TYPE1= (4/6), TYPE2 = (21 / 24)}
    //TYPE1= (1+3 / 2+4), TYPE2 = ((5+7+9) / (6+8+10))
    public static Map<String, Double> myFunction(Stream<TestRecord> testRecordStream) {
        //TODO
        return null;
    }
}

I want to return a map like {TYPE1= (0.66), TYPE2 = (0.875)}, in the above example. I am not allowed to use for loops or the forEach function.


Solution

  • 1. Using Collectors.teeing

    If you are using Java 12+ you can use Collectors.teeing to solve this.

    We pass two Collectors to Collectors.teeing; the first one sums the return values of function a() and the second for function b(). In the merger function of teeing, we divide the summed values.

    public static Map<String, Double> myFunction(Stream<TestRecord> testRecordStream) {
        return testRecordStream
                .collect(Collectors.groupingBy(TestRecord::type,
                        Collectors.teeing(
                                Collectors.summingDouble(TestRecord::a),
                                Collectors.summingDouble(TestRecord::b),
                                (a, b) -> a / b)
                ));
    }
    

    2 Using Collectors.reducing + Collectors.collectingAndThen

    In case, you are using Java version < 12, then you can use reduction mechanism to reduce (here it is sum) the values for a given type. But this would require a simple auxiliary class.

    private class Pair {
        private final int l;
        private final int r;
        //Constructors, getters skipped
    }
    
    

    The reducing Collector is used from within a collectingAndThen Collector.

    • The first argument of the reducing collector is the identity value (new Pair(0, 0)).
    • The second argument maps an instance of TestRecord to a Pair.
    • The final argument merges two Pair instances by summing the left and right value (which is l and r here).

    Finally, after reducing, we would have a single Pair instance whose l is equal to the sum of all a() function values and r is the sum of all b() function values (for a given type). We divide these two in the finisher of the collectingAndThen Collector.

    public static Map<String, Double> myFunction(Stream<TestRecord> testRecordStream) {
        return testRecordStream
                .collect(Collectors.groupingBy(TestRecord::type,
                        Collectors.collectingAndThen(
                                Collectors.reducing(new Pair(0, 0),
                                        testRecord -> new Pair(testRecord.a(), testRecord.b()),
                                        (p1, p2) -> new Pair(p1.l + p2.l, p1.r + p2.r)),
                                pair -> (double) pair.l / pair.r
                        )));
    }