Definition
- Running/cumulative sum: each output element is the sum of all input elements up to and including that position.
- Streams are stateless by design: the pipeline model discourages sharing mutable state between elements.
- AtomicInteger hack: capture an
AtomicIntegerin a map lambda and useaddAndGetto accumulate, but this only works sequentially. - Arrays.parallelPrefix: computes prefix sums on a primitive array efficiently and correctly in parallel.
- For-loop: often the clearest and most efficient approach for running totals.
Rajesh at the National Bank of India reconciles daily deposits. Each row shows today's amount, but the manager wants a running total: row 1 shows day-1 total, row 2 shows day-1 + day-2, row 3 shows day-1 + day-2 + day-3. Each row depends on all previous rows, which is inherently stateful.
Understanding the Concepts
What does this question ask?
The interviewer wants to see if you understand when streams are NOT the best tool. Running sums are inherently stateful: each element depends on all previous elements. This tests awareness of stream limitations.
How do you compute running sum with AtomicInteger (sequential only)?
package com.bank.service;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
public class RunningSumDemo {
public static void main(String[] args) {
List<Integer> dailyDeposits = List.of(100, 200, 150, 300, 250);
AtomicInteger accumulator = new AtomicInteger(0);
List<Integer> runningSum = dailyDeposits.stream()
.map(accumulator::addAndGet)
.toList();
System.out.println("Daily: " + dailyDeposits);
System.out.println("Running: " + runningSum);
}
}
Daily: [100, 200, 150, 300, 250]
Running: [100, 300, 450, 750, 1000]
This works for sequential streams but produces wrong results in parallel because AtomicInteger.addAndGet is not associative across partitioned chunks.
Why is the AtomicInteger approach unsafe for parallel streams?
// WRONG for parallel: order of addAndGet is unpredictable
AtomicInteger acc = new AtomicInteger(0);
List<Integer> broken = dailyDeposits.parallelStream()
.map(acc::addAndGet) // threads call addAndGet in arbitrary order
.toList();
// Result is unpredictable: [100, 700, 450, 300, 1000] or similar
How do you use Arrays.parallelPrefix?
For primitive arrays, Arrays.parallelPrefix computes prefix sums correctly, even in parallel.
int[] deposits = {100, 200, 150, 300, 250};
Arrays.parallelPrefix(deposits, Integer::sum);
System.out.println(Arrays.toString(deposits));
// [100, 300, 450, 750, 1000]
This modifies the array in place. It uses a divide-and-conquer algorithm that works correctly in parallel.
When is a for-loop better than a stream?
// For-loop: clear, efficient, obviously correct
List<Integer> dailyDeposits = List.of(100, 200, 150, 300, 250);
List<Integer> runningSum = new ArrayList<>();
int sum = 0;
for (int deposit : dailyDeposits) {
sum += deposit;
runningSum.add(sum);
}
// [100, 300, 450, 750, 1000]
The for-loop is clearer, faster (no boxing overhead), and obviously correct. Not everything should be a stream. The interviewer often wants to hear you say: "A for-loop is better here because running sums are inherently stateful."
How is this different from reduce?
| Operation | reduce | running sum |
|---|---|---|
| Output | Single value (total) | List of cumulative values |
| Example input | [100, 200, 150] | [100, 200, 150] |
| Example output | 450 | [100, 300, 450] |
| Stateful? | No (associative folding) | Yes (each element depends on previous) |
// reduce: single total
int total = dailyDeposits.stream().reduce(0, Integer::sum);
// 1000
// running sum: list of cumulative totals
// [100, 300, 450, 750, 1000]
What are common mistakes?
Mistake: using the AtomicInteger hack in a parallel stream.
// WRONG: parallel + stateful = wrong results
AtomicInteger acc = new AtomicInteger(0);
List<Integer> wrong = dailyDeposits.parallelStream()
.map(acc::addAndGet)
.toList();
// CORRECT: use sequential + AtomicInteger, or for-loop, or parallelPrefix
What are common interview follow-up questions?
- "Can you do this purely functionally?" Use
Stream.iterateorscanLeft(available in Vavr but not in standard Java). - "How would you compute running average?" Track both running sum and count, divide at each step.
- "Is there a stream method like scanLeft in Scala?" No, Java streams lack a built-in scan operation.
Visual Overview
Input: [100, 200, 150, 300, 250] Running sum computation: ┌──────────────────────────────────────┐ │ Element Accumulator Running Sum │ │ 100 0 + 100 = 100 │ │ 200 100 + 200 = 300 │ │ 150 300 + 150 = 450 │ │ 300 450 + 300 = 750 │ │ 250 750 + 250 = 1000 │ └──────────────────────────────────────┘ Output: [100, 300, 450, 750, 1000] Note: each row depends on the previous row → inherently stateful → streams are not ideal
Key Takeaway
Running sums require mutable state between elements, which streams are not naturally designed for. The AtomicInteger hack works for sequential streams but produces wrong results in parallel because the order of addAndGet calls is unpredictable.
For primitive arrays, Arrays.parallelPrefix(array, Integer::sum) computes prefix sums correctly and efficiently, even in parallel. For general use, a for-loop is often the clearest and most efficient solution.
This is a trick question in interviews. The interviewer wants to see if you know when streams are NOT the right tool. Saying 'a for-loop is better here because running sums are inherently stateful' demonstrates mature judgment.
Remember one sentence: running sums are inherently stateful, so prefer a for-loop or Arrays.parallelPrefix over a stream hack with AtomicInteger.
---