What happens when an int overflows, and why does Math.abs sometimes return a negative number?

Asked oftenjunior0–8 yrs9 min read

Nothing happens — it wraps silently. Integer.MAX_VALUE + 1 is Integer.MIN_VALUE, with no exception and no warning. And because the range is asymmetric, MIN_VALUE has no positive counterpart, so Math.abs(Integer.MIN_VALUE) returns MIN_VALUE — a negative result from a method that promises a magnitude. Use long, or Math.addExact when you need it to fail loudly.

The Answer

  • An int is 32 bits, holding −2,147,483,648 to 2,147,483,647. Arithmetic past either end wraps around — no exception, no warning, no log line.
  • Integer.MAX_VALUE + 1 is Integer.MIN_VALUE. The bits carry into the sign position and the number becomes negative.
  • The range is asymmetric: there is one more negative value than positive, because zero takes a slot on the positive side.
  • So MIN_VALUE has no positive counterpart, and Math.abs(Integer.MIN_VALUE) returns Integer.MIN_VALUE — negative. Documented, and a real source of bugs.
  • Fixes, in order of preference: use long; use Math.addExact and friends, which throw; or check the bound before the operation.
  • Integer division truncates toward zero, so -7 / 2 is -3 and -7 % 2 is -1.

Understand It

Wrapping is the specified behaviour

Java's integer arithmetic is defined as two's-complement with wraparound. It is not undefined behaviour, it is not a warning, and it is not checked — because checking every addition would cost performance on the most common operation in any program.

Compiled and run on this buildEdit and run
int max = Integer.MAX_VALUE;

System.out.println("MAX_VALUE     = " + max);
System.out.println("MAX_VALUE + 1 = " + (max + 1));
System.out.println("MIN_VALUE - 1 = " + (Integer.MIN_VALUE - 1));
Output
MAX_VALUE     = 2147483647
MAX_VALUE + 1 = -2147483648
MIN_VALUE - 1 = 2147483647

Adding one to the largest int gives the smallest. The arithmetic is exact modulo 2³²; it is your interpretation of the bits as signed that flips.

The asymmetry, and what it does to Math.abs

32 bits give 4,294,967,296 distinct values. Split them around zero and you cannot do it evenly: zero occupies one of the "positive" slots, so the range runs from −2,147,483,648 to +2,147,483,647. One more negative than positive.

That one missing value has a consequence that surprises everyone:

Compiled and run on this buildEdit and run
System.out.println("Math.abs(MIN_VALUE) = " + Math.abs(Integer.MIN_VALUE));
System.out.println("-MIN_VALUE          = " + (-Integer.MIN_VALUE));
System.out.println("is the result < 0?  = " + (Math.abs(Integer.MIN_VALUE) < 0));
Output
Math.abs(MIN_VALUE) = -2147483648
-MIN_VALUE          = -2147483648
is the result < 0?  = true

Math.abs returned a negative number. It is not a bug — the positive value 2147483648 does not exist as an int, so there is nothing correct to return, and the method documents that it returns the argument unchanged in this one case. Negation has the same problem: -MIN_VALUE is MIN_VALUE.

This breaks real code. The pattern Math.abs(key.hashCode()) % buckets is common and wrong: hashCode() can return Integer.MIN_VALUE, abs hands it straight back, and % then yields a negative index, producing an ArrayIndexOutOfBoundsException on a good day and silent misbehaviour on a bad one. Math.floorMod(hash, buckets) is the correct form.

The overflow that looks like a constant

This is the shape that reaches production, because it looks like arithmetic a compiler would surely fold correctly:

Compiled and run on this buildEdit and run
int millisPerDay = 24 * 60 * 60 * 1000;
int millisPer30Days = 30 * 24 * 60 * 60 * 1000;

System.out.println("ms per day     = " + millisPerDay);
System.out.println("ms per 30 days = " + millisPer30Days + "   (should be 2,592,000,000)");
System.out.println("as long        = " + 30L * 24 * 60 * 60 * 1000);
Output
ms per day     = 86400000
ms per 30 days = -1702967296   (should be 2,592,000,000)
as long        = 2592000000

Every operand is a small, obviously-safe literal. The product is not. 2,592,000,000 exceeds Integer.MAX_VALUE by about 20%, so the expression wraps to a negative number — and as a timeout or a cache TTL, a negative duration behaves very differently from a long one.

The fix is one character: make the first operand a long. Order matters, because the expression is evaluated left to right and the type is decided per operation. 30 * 24 * 60 * 60 * 1000L overflows in int first and then widens the already-wrong result.

Division truncates, and it truncates toward zero

Not floor — toward zero, which differs for negatives:

Compiled and run on this buildEdit and run
System.out.println("7 / 2   = " + (7 / 2));
System.out.println("-7 / 2  = " + (-7 / 2));
System.out.println("-7 % 2  = " + (-7 % 2));
System.out.println("(int) 3.9  = " + (int) 3.9);
System.out.println("(int) -3.9 = " + (int) -3.9);
System.out.println("(int) 3_000_000_000L = " + (int) 3_000_000_000L);
Output
7 / 2   = 3
-7 / 2  = -3
-7 % 2  = -1
(int) 3.9  = 3
(int) -3.9 = -3
(int) 3_000_000_000L = -1294967296

-7 / 2 is -3, not -4, and % follows so that (a / b) * b + a % b == a holds. The consequence: % can return a negative number, so hash % buckets is unsafe as an index even before Math.abs gets involved. Math.floorMod gives the non-negative answer people expect.

The last line is the other silent truncation: casting a long to int keeps the low 32 bits and discards the rest, with no complaint.

Reference

Making overflow loud, when correctness matters more than speed:

// Throws ArithmeticException("integer overflow") instead of wrapping
int sum     = Math.addExact(a, b);
int product = Math.multiplyExact(a, b);
int diff    = Math.subtractExact(a, b);
int negated = Math.negateExact(a);          // throws on MIN_VALUE
int narrowed = Math.toIntExact(someLong);   // throws if it will not fit

// Saturating instead of throwing — clamps to MIN_VALUE / MAX_VALUE
int clamped = Math.clamp(someLong, Integer.MIN_VALUE, Integer.MAX_VALUE);
Compiled and run on this buildEdit and run
try { Math.addExact(Integer.MAX_VALUE, 1); }
catch (ArithmeticException e) { System.out.println("addExact     -> " + e.getMessage()); }

try { Math.negateExact(Integer.MIN_VALUE); }
catch (ArithmeticException e) { System.out.println("negateExact  -> " + e.getMessage()); }

try { Math.toIntExact(3_000_000_000L); }
catch (ArithmeticException e) { System.out.println("toIntExact   -> " + e.getMessage()); }
Output
addExact     -> integer overflow
negateExact  -> integer overflow
toIntExact   -> integer overflow

The patterns to fix on sight:

// WRONG                                    // RIGHT
Math.abs(hash) % buckets                    Math.floorMod(hash, buckets)
(lo + hi) / 2                               lo + (hi - lo) / 2
x - y                    // comparator      Integer.compare(x, y)
30 * 24 * 60 * 60 * 1000                    30L * 24 * 60 * 60 * 1000
int total = price * qty;                     long total = (long) price * qty;
count++                  // may wrap        Math.incrementExact(count)

Choosing a type by range:

TypeRangeUse when
int±2.1 billioncounts, indices, ids under ~2 billion
long±9.2 quintillionmoney in minor units, timestamps in millis, byte counts, any id that grows
BigIntegerunboundedcryptography, factorials, anything provably unbounded

If a value is a count of things that could exceed two billion — rows, bytes, milliseconds over a long span — it is a long. Two billion is not a large number any more.

Scenarios

A timeout computed in int milliseconds. days * 24 * 60 * 60 * 1000 overflows past 24 days, producing a negative timeout. Depending on the API that is an immediate return, an exception, or an effectively infinite wait — three different bugs from one expression.

Hash bucket index going negative. Math.abs(hashCode()) % size works for every key until one hashes to Integer.MIN_VALUE, then abs returns it unchanged and the index is negative. Rare, data-dependent, and it looks like memory corruption rather than arithmetic.

A row counter in a growing table. int count works for years and then quietly wraps, and the report shows a negative total. The bug was written years before it fired, which is the characteristic shape of overflow bugs.

Where int is still right. Array indices — Java arrays cannot exceed Integer.MAX_VALUE elements anyway — loop counters over bounded collections, and ordinary domain values with a known ceiling. Reaching for long everywhere costs memory and readability for no benefit.

Interviewer's Next Move

1. "Why doesn't the JVM throw on overflow?" Because integer addition is the single most common operation in any program, and checking each one would cost measurable performance for a case most code never hits. The JLS therefore defines wraparound rather than leaving it undefined, and offers Math.addExact for when you want the check. It is a deliberate trade, not an oversight.

2. "Explain Math.abs(Integer.MIN_VALUE)." The range is asymmetric — one more negative value than positive — so +2147483648 is not an int. There is no correct answer to return, and the method documents that it returns the argument unchanged. Use Math.absExact to have it throw, or work in long.

3. "Is -7 % 2 equal to 1 or -1?" -1. Java truncates division toward zero and defines % to keep (a / b) * b + a % b == a true, so the remainder takes the sign of the dividend. Math.floorMod(-7, 2) gives 1, which is what you want for wrapping an index.

4. "Where does this interact with binary search?" (lo + hi) / 2 overflows once both indices are large, producing a negative mid and an ArrayIndexOutOfBoundsException. It was in the JDK's own Arrays.binarySearch for nine years. lo + (hi - lo) / 2 cannot overflow, because hi - lo is a difference of two non-negative ints.

5. "How do you detect this in an existing codebase?" Look for multiplication of int values where the factors are durations, sizes or counts, and for Math.abs feeding a %. SpotBugs and Error Prone both report the abs-then-modulo pattern. Overflow itself is not statically detectable in general, which is why range-appropriate types beat auditing.

Check Yourself

Integer.MAX_VALUE + 1 is negative. Where did the value go? Nowhere — the arithmetic is exact modulo 2³². The carry lands in the sign bit, so the same bit pattern that means "just over two billion" as unsigned means "minus two billion" as signed. Java's ints are signed, so that is what you see.

Why is 30 * 24 * 60 * 60 * 1000 wrong but 30L * 24 * 60 * 60 * 1000 right? Each multiplication takes the wider of its two operand types. Starting with a long makes every subsequent product a long, so nothing overflows. Putting the L at the end is too late: the int product has already wrapped and the widening preserves the wrong value.

Math.abs(hashCode()) % buckets produced a negative index. Explain and fix. The hash was Integer.MIN_VALUE, which abs returns unchanged because its positive counterpart is not representable; % then yielded a negative remainder. Use Math.floorMod(hashCode(), buckets), which is non-negative for any input.

Where this question goes next

Every runnable example above was compiled and executed against openjdk 21.0.11 on this build, and its output diffed against what this page claims. Last updated 2026-09-11.