Serguey Asael Shinder: int addition wraps silently, so an exclusive end after Integer.MAX_VALUE is negative
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

Three fixes, depending on what you want.
- Fail at the arithmetic.
Math.addExact(int, int)returns the sum or throwsArithmeticExceptionif the result overflows anint(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
- Do the arithmetic in a wider type. If the value is legitimate, compute in
longand narrow only where anintis really required, withMath.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.
- Do not convert the range at all. For iteration,
IntStream.rangeClosed(startInclusive, endInclusive)takes an inclusive end, soIntStream.rangeClosed(0, Integer.MAX_VALUE)needs no+ 1anywhere (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.