skip to content

When you sum a large IntStream the result can be wrong even though no exception is thrown. Why, and how does the design of primitive streams shape how you'd prevent it?

level: seniorimportance: nice to knowfreq 28%

answer

  1. IntStream.sum() returns int (32-bit) -> silent overflow
  2. Java int + wraps, no exception (unlike Math.addExact)
  3. fix: asLongStream().sum() or mapToLong(...).sum()
  4. extreme magnitudes -> BigInteger via mapToObj/reduce
  5. summaryStatistics().getSum() is already a long

basics

~20 s

IntStream.sum() returns an int, which is only 32 bits. If the total exceeds about 2.1 billion it silently wraps around (overflows) to a wrong, possibly negative number — no error is thrown. Fix it by widening first with asLongStream().sum(), or mapToLong, so the total is a 64-bit long.

solid answer

~50 s

IntStream.sum() accumulates and returns an int, so the running total is bound by 32-bit range (max ~2.1 billion). Java integer arithmetic wraps silently on overflow rather than throwing, so a sum of many or large ints can quietly produce a wrong or negative result. Because primitive streams are typed by their element width, the type system won't auto-promote for you — sum() of an IntStream is always an int. The idiomatic prevention is to widen the stream before summing: IntStream.asLongStream().sum() or mapToLong(...).sum() returns a long (64-bit), giving headroom; for truly unbounded magnitudes you'd map to BigInteger via mapToObj and reduce. This is a deliberate consequence of the no-boxing, width-specialized design: you get speed and a precise numeric type, but you, not the runtime, are responsible for choosing a wide enough accumulator. average() is less affected since it returns a double, but very large counts can still lose precision.

code

java · 13 lines
java
// Each element is large; the int total overflows silently
int badTotal = IntStream.of(2_000_000_000, 2_000_000_000)
                        .sum();          // wraps to a negative number!
System.out.println(badTotal);            // -294967296 (wrong)

// Fix: widen to long BEFORE summing
long goodTotal = IntStream.of(2_000_000_000, 2_000_000_000)
                          .asLongStream()
                          .sum();         // 4_000_000_000 (correct)

// Fail loudly instead, via Math.addExact:
// IntStream.of(2_000_000_000, 2_000_000_000).reduce(0, Math::addExact);
// -> throws ArithmeticException: integer overflow

go deeper

for a junior

Knows IntStream.sum() can overflow because int is 32-bit and that using a long avoids it.

for a middle

Explains silent two's-complement wraparound (no exception), recognizes a negative sum as the symptom, and applies asLongStream()/mapToLong as the fix.

for a senior

Connects the bug to the width-specialized, no-boxing design (accumulator width is the caller's responsibility), and chooses among long widening, BigInteger, and Math.addExact by requirements.

for a principal

Reasons about the trade-off the API made (predictable unboxed types vs. auto-promotion), reviews numeric pipelines for overflow/precision risk, and sets team conventions (default to long sums, summaryStatistics) to prevent the class of bug.

## Background: fixed-width integers wrap silently A Java `int` is a **32-bit two's-complement** integer: it can represent values from `-2,147,483,648` to `2,147,483,647` (`Integer.MIN_VALUE`..`Integer.MAX_VALUE`). Ordinary `+` arithmetic on `int`s does **not** check for overflow — when a result exceeds the range it **wraps around** (modular arithmetic). For example `Integer.MAX_VALUE + 1` becomes `Integer.MIN_VALUE` (a large negative number). No exception, no warning — just a wrong value. (`Math.addExact` *does* throw, but the `+` operator and stream `sum()` do not.) ## Why IntStream.sum() inherits this `IntStream.sum()` is defined to return an **`int`**. Internally it keeps a running `int` accumulator and adds each element. So summing a stream whose total exceeds ~2.1 billion (e.g. 3 million elements averaging 1000, or a few elements near `MAX_VALUE`) **overflows silently** and returns garbage — often negative. The pipeline runs without error; only the number is wrong. This is one of the most common subtle bugs in numeric stream code. ## How the design forces the responsibility onto you Primitive streams are **width-specialized**: `IntStream` is *about* `int`s, and its `sum()` returns an `int` by contract. The type system will **not** silently promote the accumulator to `long` for you — that would contradict the explicit, no-boxing, predictable-type philosophy of these streams. The flip side of getting an exact, unboxed numeric type is that **choosing a wide-enough accumulator is your job**. The design gives you the tools to widen explicitly; it does not guess. ## The fixes, in order of magnitude 1. **Widen to `long` (the usual fix).** Convert the stream so the accumulator is 64-bit before summing: - `intStream.asLongStream().sum()` — lossless widening of each `int` to `long`; `sum()` now returns a `long` (range ~9.2 quintillion). - or `objects.stream().mapToLong(o -> o.getCount()).sum()` — extract as `long` from the start. A `long` accumulator handles realistic totals of 32-bit element values. 2. **`long` can still overflow** if the data is extreme. For totals beyond 64 bits, map to **`BigInteger`** (arbitrary precision) and reduce: ```java BigInteger total = ints.stream() .mapToObj(BigInteger::valueOf) .reduce(BigInteger.ZERO, BigInteger::add); ``` This never overflows but is slower and re-introduces objects. 3. **Detect rather than tolerate.** If you'd rather fail loudly, reduce with `Math.addExact`, which throws `ArithmeticException` on overflow: ```java int total = ints.stream().reduce(0, Math::addExact); // throws if it would overflow ``` ## What about average() / other ops? - `average()` returns an `OptionalDouble` (a 64-bit floating-point mean), so it doesn't overflow the way integer `sum()` does — but `double` has only ~15-16 significant decimal digits, so with an enormous count or huge magnitudes you can lose **precision** (rounding), a different failure mode. - `count()` returns a `long`, so counting is safe to ~9.2 quintillion. - `summaryStatistics()`'s `getSum()` already returns a **`long`** for `IntStream`/`LongStream`, so using it is itself an overflow-safer way to get an int-stream sum. ## The takeaway The bug is real and silent: `IntStream.sum()` is an `int`. The width-specialized, no-boxing design that makes primitive streams fast also makes **accumulator width an explicit choice**. Default to `asLongStream().sum()` (or `mapToLong`) whenever the total could plausibly exceed two billion; escalate to `BigInteger` or `Math.addExact` when correctness must be guaranteed or loudly enforced.

  • Your IntStream.sum() of positive counts returns a negative number. What happened and what's the one-line fix?
    The 32-bit int accumulator overflowed past Integer.MAX_VALUE and wrapped to negative; no exception is thrown for + overflow. Fix: widen before summing — IntStream.asLongStream().sum() (or build it with mapToLong) so the total is a 64-bit long.
  • If you'd rather fail loudly than widen, how can a stream sum throw on overflow?
    Reduce with Math.addExact instead of sum(): ints.reduce(0, Math::addExact). Math.addExact throws ArithmeticException when the int result would overflow, turning a silent wrap into an explicit failure.

saying these in an interview costs you the question

  • Assuming IntStream.sum() throws or auto-promotes to long on overflow — it silently wraps.
  • Believing a negative sum of positive values is impossible — it's the classic overflow symptom.
  • Thinking long can never overflow — it can for extreme data; use BigInteger then.
  • Treating average() as fully safe — it avoids integer overflow but can lose double precision.

context