SSerguey Asael Shinder
Java coding notes: the JVM, and writing software that lasts

Serguey Asael Shinder: int addition wraps silently, so an exclusive end after Integer.MAX_VALUE is negative

· by Serguey Asael Shinder / Serguey Shinder

Code that stores ranges as start inclusive, end exclusive has to compute end + 1 somewhere. For every value but one, that is harmless. For Integer.MAX_VALUE it produces a negative number, and Java does not tell you.

int last = Integer.MAX_VALUE;     // inclusive end supplied by a caller
int endExclusive = last + 1;      // -2147483648, no exception
for (int i = 0; i < endExclusive; i++) {
    // never runs: 0 < -2147483648 is false
}

Why it happens. The Java Language Specification defines integer addition as wrapping: if an int addition overflows, the result is the low-order 32 bits of the true sum in two's complement, and no exception is thrown (JLS §15.18.2, https://docs.oracle.com/javase/specs/jls/se21/html/jls-15.html#jls-15.18.2). Integer.MAX_VALUE + 1 is therefore exactly Integer.MIN_VALUE. The compiler accepts it, the runtime accepts it, and the bug shows up wherever the negative value is used next — an empty loop, a negative array size, a sign check that fires in the wrong place, or a crash far away from the line that caused it.

The mirror image is the loop that never ends:

for (int i = 0; i <= Integer.MAX_VALUE; i++) { ... }  // i wraps to MIN_VALUE; condition is always true
Serguey Asael Shinder: int addition wraps silently, so an exclusive end after Integer.MAX_VALUE is negative
int addition wraps silently, so an exclusive end after Integer.MAX_VALUE is negative — Serguey Asael Shinder

Three fixes, depending on what you want.

  1. Fail at the arithmetic. Math.addExact(int, int) returns the sum or throws ArithmeticException if the result overflows an int (https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Math.html#addExact(int,int)). This is the right choice when an input that large is a caller error: you want the error at the boundary, with a stack trace pointing at the cause.
int endExclusive = Math.addExact(last, 1);   // throws instead of going negative
  1. Do the arithmetic in a wider type. If the value is legitimate, compute in long and narrow only where an int is really required, with Math.toIntExact(long), which throws if the value does not fit (https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Math.html#toIntExact(long)).
long endExclusive = (long) last + 1;          // 2147483648, correct

Note the cast on last: (long) (last + 1) would overflow first and widen the wrong answer.

  1. Do not convert the range at all. For iteration, IntStream.rangeClosed(startInclusive, endInclusive) takes an inclusive end, so IntStream.rangeClosed(0, Integer.MAX_VALUE) needs no + 1 anywhere (https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/stream/IntStream.html#rangeClosed(int,int)).

The same shape in a midpoint. (low + high) / 2 overflows when both are large and positive. OpenJDK's Arrays.binarySearch implementations compute the midpoint as (low + high) >>> 1 (https://github.com/openjdk/jdk/blob/master/src/java.base/share/classes/java/util/Arrays.java), which treats the wrapped sum as unsigned and stays correct for non-negative indexes.

Where to look in your code. Search for + 1 and - 1 next to anything named end, last, max, limit or size, especially where a value comes from configuration or a request. The values that break this code are exactly the ones nobody types in a unit test. Bitcoin Core merged a fix this week for the same case in C++: a wallet range ending at 2^31-1 whose exclusive end turned negative and crashed the node (https://github.com/bitcoin/bitcoin/pull/35989). The language is different; the arithmetic is the same.

No JDK runs on the machine this note was written on; the behaviour described is the one specified in the linked JLS section and javadoc.