Fab
Fab

Reputation: 1215

Java - Perform a cumulative sum using forEach or the Stream API

here my current code to perform a cumulative sum over a hash table Map< String,Double>

START.forEach((k,v)->{

            sum += v;
            END.put(k, sum);

        });

or, alternately,

END= START.entrySet()
                .stream()
                .collect(
                        Collectors.toMap(entry -> entry.getKey(), 
                                entry -> {
                                    sum += entry.getValue();
                                    return sum;
                                }));

But I have the following error:

Local variable sum defined in an enclosing scope must be final or effectively final

How could I fix it?

I wouldn't want to use the standard for loop like that:

Iterator it = START.entrySet().iterator();
        double sum = 0;
        while (it.hasNext()) {
            Map.Entry pair = (Map.Entry)it.next();
            String key = (String) pair.getKey();
            Double value = (Double) pair.getValue();
            sum+=value;
            END.put(date, sum);
        }



START
------------
|first | 1 |
|second| 5 |
|third | 4 |

END
|first | 1  | 
|second| 6  |
|third | 10 |

Upvotes: 4

Views: 6268

Answers (4)

Mincong Huang
Mincong Huang

Reputation: 5552

Entries' order is important for cumulative sum. If you're using HashMap as implementation, it makes no guarantees as to the order of the map; in particular, it does not guarantee that the order will remain constant over time. So I recommend you to use another implementation, like LinkedHashMap. It use hash table and linked list implementation of the Map interface, with predictable iteration order.

Map<String, Double> map = new LinkedHashMap<>();
map.put("first", 1.0);
map.put("second", 5.0);
map.put("third", 4.0);

Use atomic reference to avoid "final" problem. In Java, you can't use non-final variables in lambda as well as in anonymous inner classes. That's why you got the message "Local variable sum defined in an enclosing scope must be final or effectively final". Then, you can define the binary operator as (x, y) -> x + y, since you wish to summarize the current entry's value with the previous cumulative sum.

AtomicReference<Double> atomicSum = new AtomicReference<>(0.0);
map.entrySet().forEach(e -> e.setValue(
    atomicSum.accumulateAndGet(e.getValue(), (x, y) -> x + y)
));

Here's the final code.

Map<String, Double> map = new LinkedHashMap<>();
map.put("first", 1.0);
map.put("second", 5.0);
map.put("third", 4.0);

AtomicReference<Double> atomicSum = new AtomicReference<>(0.0);
map.entrySet().forEach(e -> e.setValue(
    atomicSum.accumulateAndGet(e.getValue(), (x, y) -> x + y)
));

// tested in JUnit
assertEquals(10.0, atomicSum.get(), 0.0001);
assertEquals(1.0, map.get("first"), 0.0001);
assertEquals(6.0, map.get("second"), 0.0001);
assertEquals(10.0, map.get("third"), 0.0001);

Upvotes: 4

D. Werle
D. Werle

Reputation: 186

You can use an java.util.concurrent.DoubleAdder and Collectors#toMap like this:

final Map<String, Double> START = new HashMap<>();
START.put("first", 1.0);
START.put("second", 5.0);
START.put("third", 4.0);

System.out.println(START.toString());

DoubleAdder sum = new DoubleAdder();
Map<String, Double> cumulativeSum = START.entrySet().stream().sequential().collect(
    Collectors.toMap(Entry::getKey, it -> { sum.add(it.getValue()); return sum.sum(); }));

System.out.println(cumulativeSum.toString());

Upvotes: 0

SachinSarawgi
SachinSarawgi

Reputation: 2692

I do not know but this snippet of code may help you.

List<Integer> ints = new ArrayList<>();
ints.add(1);
ints.add(2);
ints.add(3);

AtomicInteger sum = new AtomicInteger(0);
ints.stream().sequential().mapToInt(sum::addAndGet).forEach(System.out::println);

Try to modify and use this code snippet in your code.

Upvotes: 0

Jeremy Grand
Jeremy Grand

Reputation: 2370

You need sum to be an AtomicLong and perform addAndGet instead of the += because, as the error said, you need sum to be final.

Upvotes: 1

Related Questions