I'm trying to implement a cumulative sum of the values of Objects from a list.
The Object looks as shown below:
public class NameValuePair {
private String name;
private int value;
}
I have a List<NameValuePair>
as input.
And the result should be also a List<NameValuePair>
with the cumulated values, i.e. every subsequent value should be summed with the total obtained so far.
How can I achieve it with Java Stream API?
An input sample 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 theDeque
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 anAssertionError
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]
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