Consistent Overflow: Two’s Complement and Modular Arithmetic
Java promises that Integer.MAX_VALUE + 1 always equals Integer.MIN_VALUE. That promise is exactly what let a bug hide inside the JDK’s own binary search for nine years.
Try this in any Java program: take Integer.MAX_VALUE, which is 2,147,483,647, and add 1. You will not get an exception. You will not get a warning. You will get -2,147,483,648, the smallest possible int, and the JVM will consider that a perfectly normal, successful addition. In C, the same line of code has no defined outcome at all. The compiler is legally allowed to assume it never happens, and to generate code accordingly.
That single difference, one language guaranteeing a specific wraparound result, the other guaranteeing nothing, shapes how bugs show up, how compilers optimize, and how much you can actually trust a line of arithmetic. It’s also more subtle than “defined is safer than undefined.” Java’s guarantee has bitten people too, sometimes for decades at a time.
What’s Actually Happening at the Bit Level
Java’s int is a 32-bit signed two’s complement number, a representation spelled out precisely in the Java Language Specification’s section on integral types. Two’s complement works by treating the top bit as a sign indicator and letting arithmetic run as if the number line wrapped around in a circle instead of running off to infinity in both directions. Add 1 to the largest positive value the bits can represent, and you land exactly on the most negative value, because the extra bit that would have carried the result past the 32-bit boundary simply gets thrown away.
This is modular arithmetic in the most literal sense: every int operation in Java is really happening modulo 2³². The result you see is always mathematically correct, just reduced into the range that 32 bits can hold, the same way a 12-hour clock quietly reduces “15 o’clock” down to 3.
Why Java Made Overflow a Guarantee
Java’s designers didn’t leave this to chance. The JLS specifies, operator by operator, exactly which ones can overflow and exactly what happens when they do.
| Operator | Can overflow? | Behavior |
|---|---|---|
| +, -, *, ++, — | Yes | Silently wraps using two’s complement, no exception |
| / (division) | Yes, one specific case | Integer.MIN_VALUE / -1 wraps back to Integer.MIN_VALUE |
| % (remainder) | No | Cannot overflow by definition |
| <<, >>, &, |, ^, ~ | No | Bitwise operators don’t have an overflow concept |
| Comparisons (<, >, ==, etc.) | No | Always well-defined regardless of the operands |
Every one of those “yes” rows behaves identically on every JVM, on every operating system, on every CPU architecture. That consistency was a deliberate trade Java’s designers made, and once you see why C went the other way, the reasoning becomes clearer.
Why C Deliberately Refuses to Promise Anything
Signed integer overflow in C is undefined behavior, meaning the C standard places no requirement whatsoever on what happens next. As the SEI CERT C coding standard puts it, an implementation is free to wrap the value, trap with a hardware fault, or do something else entirely, and a conforming compiler doesn’t even have to be consistent about which choice it makes from one context to the next.
This isn’t an oversight. It exists because assuming overflow can never happen unlocks real compiler optimizations. Harvard’s introductory systems course notes put a number on it: SPEC CPU 2006, a standard benchmark suite, runs about 30% slower when overflow checking is forced on, because the compiler loses its ability to assume, for instance, that x + 1 > x is always true.
Relative Runtime: SPEC CPU 2006 With and Without Overflow Checks Enforced

The downside is that this freedom can eliminate the exact safety checks a programmer wrote on purpose. The GNU Autoconf manual gives a genuinely alarming, real illustration: a password-checking function that increments a mismatch counter and aborts once it hits INT_MAX. If the standard’s undefined-behavior rule is taken to its logical conclusion, a sufficiently aggressive compiler is permitted to notice that the counter overflowing is undefined, conclude the overflow branch is therefore unreachable, and optimize away the abort call along with the password check that guards it. That is the real cost of undefined behavior: it isn’t just “unpredictable,” it can silently delete code you were counting on.
The Bug That Proves Java’s Guarantee Cuts Both Ways
Here’s where it gets genuinely interesting, because a defined, wraparound-safe overflow does not automatically mean “no bugs.” It means a specific, predictable kind of bug instead.
In 2006, Joshua Bloch, the engineer who wrote much of the Java Collections framework, published a post titled “Extra, Extra – Read All About It: Nearly All Binary Searches and Mergesorts are Broken.” The classic midpoint calculation in binary search, mid = (low + high) / 2, looks completely correct and had been taught that way for decades. It fails the moment low + high exceeds Integer.MAX_VALUE, which requires an array of roughly 2³⁰ elements, a bit over a billion, to trigger. Bloch traced the exact same bug into the JDK’s own java.util.Arrays.binarySearch, where it had sat undetected for nine years, and further back into Jon Bentley’s textbook implementation in Programming Pearls, where it had gone unnoticed for two decades before that.
Notice what actually caused the bug: not the absence of a guarantee, but a programmer’s failure to account for one that was already there. Because Java’s overflow is defined, the failure mode was at least predictable and debuggable, low + high wrapped into a negative number, and Java threw a clean ArrayIndexOutOfBoundsException pointing at the exact line. Bloch notes that in C, the equivalent bug produces an out-of-bounds array access with the undefined behavior that implies, a strictly worse failure mode. Java’s guarantee didn’t cause the bug. It just made the resulting failure honest instead of chaotic.
The fix reveals the real lesson
The accepted fix, mid = low + (high - low) / 2, never lets the intermediate sum exceed either bound in the first place. It isn’t a workaround for Java’s overflow behavior, it’s a reminder that “the language will handle overflow consistently” was never the same promise as “your formula won’t overflow.” Those are two separate concerns, and conflating them is precisely how a two-decade-old textbook bug survives code review.
When Overflow Is the Feature, Not the Bug
It’s worth balancing the binary search cautionary tale with the flip side: Java sometimes specifies overflow on purpose, because a deterministic wraparound is exactly what the algorithm wants. String.hashCode() is documented to compute s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1], and for anything longer than a short string, that polynomial overflows int arithmetic constantly. The specification doesn’t treat this as a defect, it relies on it, because a hash function only needs a well-distributed, repeatable int, not a mathematically exact one. This works only because Java guarantees the wraparound is identical on every platform; an undefined-behavior version of this same formula would produce a different hash on a different compiler, which would be catastrophic for anything relying on consistent hash codes, like persisted hash-based indexes.
Correct Math vs. What the Wrapped int Actually Stores

Math.multiplyExact(100000, 100000), which throws ArithmeticException reporting the correct product before the plain * operator would have silently wrapped it to 1,410,065,408.That gap between the two bars is the whole danger of relying on wraparound incorrectly: 100,000 times 100,000 is ten billion, but a plain int can only ever report 1,410,065,408, a number with no visible connection to the inputs that produced it. Nothing crashes. Nothing warns you. The wrong number simply becomes the input to whatever runs next.
Java vs. C, Side by Side
| Case | Java int / long | C signed int | C unsigned int |
|---|---|---|---|
| Overflow behavior | Always wraps, two’s complement, every platform | Undefined; compiler may assume it never happens | Always wraps, modulo 2ⁿ (this one is defined too) |
| Compiler can remove overflow-triggered checks? | No | Yes, legally | No |
| Failure mode when it happens by accident | A wrong-but-consistent number, or a clean exception (division, Math.*Exact) | Anything: wrong number, crash, or code that behaves as if the check never existed | A wrong-but-consistent number |
| Can you opt into checked arithmetic? | Yes, via Math.addExact, subtractExact, multiplyExact (Java 8+) | Only with non-standard flags like GCC’s -ftrapv, or manual checks | Not typically needed; wrapping is usually the intent |
The unsigned row is the detail most people miss: C’s undefined-behavior reputation applies specifically to signed overflow. Unsigned integer overflow in C has always been defined as wraparound modulo 2ⁿ, which is exactly why checksum, hashing, and cryptographic code in C reaches for unsigned types deliberately, for the same reason Java’s String.hashCode() leans on defined signed wraparound.
Detecting Overflow When You Actually Need To
Since Java 8, you don’t have to choose between “silent wraparound” and “write your own bounds check.” The Math class ships addExact, subtractExact, multiplyExact, incrementExact, decrementExact, negateExact, and toIntExact, all of which throw ArithmeticException the instant an operation would overflow, rather than returning a corrupted result. They cost a comparison or two over the plain operator, which is a trivial price in any code path that touches money, counters, identifiers, or array indices, exactly the categories where a silently wrong number is far more expensive than a thrown exception.
Quick Checklist: Working With Overflow Instead of Getting Surprised by It
- Never assume a sum of two large, unbounded values is safe just because Java’s overflow behavior is defined; defined and correct are not the same thing, as the binary search bug proves.
- Reach for
low + (high - low) / 2instead of(low + high) / 2anywhere a midpoint calculation touches attacker-controlled or simply large inputs. - Use
Math.addExact/multiplyExacton any accumulator that touches money, counts, or generated identifiers, where a wrapped value would be actively harmful rather than merely wrong. - If you’re intentionally relying on wraparound, as
String.hashCode()does, document that it’s deliberate, so nobody “fixes” it later. - Remember that C’s unsigned integers wrap predictably too; it’s specifically signed overflow that invites undefined behavior and compiler-driven surprises.
What We Have Learned
Java defines integer overflow precisely because two’s complement makes that guarantee cheap to provide and because a predictable wraparound is far easier to reason about, and to debug, than a result the compiler is free to fabricate. C leaves signed overflow undefined for the opposite reason: it buys compilers real optimization headroom, on the order of 30% on standard benchmarks, at the cost of occasionally deleting the exact safety check a programmer wrote to catch it. Neither choice eliminates overflow bugs.
Java’s own standard library carried a wraparound-triggered binary search bug for nine years, not because the language’s guarantee failed, but because a formula didn’t account for a guarantee that was working exactly as specified. The real skill isn’t picking the “safer” language, it’s knowing when overflow is a deliberate tool, as in String.hashCode(), when it’s a latent hazard waiting for a big enough input, as in naive midpoint math, and reaching for Math.addExact and its siblings whenever a wrong-but-silent number would cost more than a thrown exception.

