Home > Software design >  Cumulative Sum of Object values with Java 8 stream API
Cumulative Sum of Object values with Java 8 stream API

Time:06-04

I'm trying to implement a cumulative sum to the values of some Objects from a list

The Object is as below:

public class NameValuePair { private String name; private int value; }

I have a List<NameValuePair> as input, and I also have to return a List<NameValuePair> with the values cumulated, where every subsequent is summed with the total so far.

How can I achieve it with Java Stream API?

A sample input would be: ("a", 2), ("b", 12), ("c", 15), ("d", 20)

And the desired output would be: ("a", 2), ("b", 14), ("c", 29), ("d", 49)

CodePudding user response:

The process of accumulating values can be handled inside a collector.

In this case, there would be no need for storing the current value outside the stream-pipeline and updating it via side effects, which isn't encouraged by the API documentation.

Custom Collector

For that, we need to define a custom collector. Which could be implemented as a class implementing Collector interface, or we can make use of the static method Collector.of().

These are parameters expected by the Collector.of():

  • Supplier Supplier<A> is meant to provide a mutable container which store elements of the stream. In this case, ArrayDeque (as an implementation of the Deque interface) will be handy as a container to facilitate the convenient access to the previously added element.

  • Accumulator BiConsumer<A,T> defines how to add elements into the container provided by the supplier. In the accumulator to we need to make sure that the deque is not empty before accessing the last element. Note: pairs in the solution provided below are treated as immutable (and I've reimplemented as a record), therefore the very first pair is used as is, the others would be reinstaintiated.

  • Combiner BinaryOperator<A> combiner() establishes a rule on how to merge the two containers obtained while executing stream in parallel. This task can is sequential by its nature, it doesn't make sense splitting it into subtasks and executed in parallel. For that reason, the combiner is implemented to throw an AssertionError in case of parallel execution.

  • Finisher Function<A,R> is meant to produce the final result by transforming the mutable container. The finisher function in the code below turns the container (the deque containing the result), into an immutable list.

  • Characteristics allow providing additional information, for instance Collector.Characteristics.UNORDERED which is used in this case denotes that the order in which partial results of the reduction produced while executing in parallel is not significant. This collector doesn't require any characteristics.

Implementation

public static List<NameValuePair> accumulateValues(List<NameValuePair> pairs) {
    return pairs.stream()
        .collect(getPairAccumulator());
}

public static Collector<NameValuePair, ?, List<NameValuePair>> getPairAccumulator() {
    
    return Collector.of(
        ArrayDeque::new, // mutable container
        (Deque<NameValuePair> deque, NameValuePair pair) -> {
            if (deque.isEmpty()) deque.add(pair);
            else deque.add(new NameValuePair(pair.name(), deque.getLast().value()   pair.value()));
        },
        (left, right) -> { throw new AssertionError("should not be executed in parallel"); }, // combiner - function responsible 
        (Deque<NameValuePair> deque) -> deque.stream().toList() // finisher function
    );
}

If you're using Java 16 or above, you can implement NameValuePair as a record:

public record NameValuePair(String name, int value) {}

main()

public static void main(String[] args) {
    List<NameValuePair> pairs =
        List.of(new NameValuePair("a", 2), new NameValuePair("b", 12),
                new NameValuePair("c", 15), new NameValuePair("d", 20));

    List<NameValuePair> result = accumulateValues(pairs);
    result.forEach(System.out::println);
}

Output:

NameValuePair[name=a, value=2]
NameValuePair[name=b, value=14]
NameValuePair[name=c, value=29]
NameValuePair[name=d, value=49]

A link to Online Demo

CodePudding user response:

You can use an AtomicInteger to store the cumulated value:

import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;


....


List<NameValuePair> list = List.of( new NameValuePair("a", 2),
                                    new NameValuePair("b", 12),
                                    new NameValuePair("c", 15),
                                    new NameValuePair("d", 20));

AtomicInteger ai = new AtomicInteger(0);

List<NameValuePair> result = list.stream()
                                 .map(nvp -> new NameValuePair(nvp.getName(), ai.addAndGet(nvp.getValue())))
                                 .collect(Collectors.toList());

CodePudding user response:

public static void main(String[] args) {
    List<NameValuePair> nameValuePairs = new ArrayList<>();

    nameValuePairs.add(new NameValuePair("name1", 1));
    nameValuePairs.add(new NameValuePair("name2", 2));
    nameValuePairs.add(new NameValuePair("name3", 3));
    nameValuePairs.add(new NameValuePair("name4", 4));
    nameValuePairs.add(new NameValuePair("name5", 5));
    nameValuePairs.add(new NameValuePair("name6", 6));
    nameValuePairs.add(new NameValuePair("name7", 7));
    nameValuePairs.add(new NameValuePair("name8", 8));
    nameValuePairs.add(new NameValuePair("name9", 9));
    nameValuePairs.add(new NameValuePair("name10", 10));

    AtomicInteger sum = new AtomicInteger();
    List<NameValuePair> accumulator =
            nameValuePairs
            .stream()
            .map(nvp -> new NameValuePair(nvp.getName(), sum.addAndGet(nvp.getValue())))
            .collect(Collectors.toList());

    for (NameValuePair nameValuePair : accumulator) {
        System.out.println(nameValuePair.getName()   ": "   nameValuePair.getValue());
    }
}

The output is:

name1: 1
name2: 3
name3: 6
name4: 10
name5: 15
name6: 21
name7: 28
name8: 36
name9: 45
name10: 55
  • Related