Topic 1.3
Integer Ranges and Overflow
In one line
Java integers have a fixed number of bits, so when a result is too big it silently wraps around to the other end of the range: Integer.MAX_VALUE + 1 is Integer.MIN_VALUE. Knowing how two's complement works lets you predict, detect and prevent overflow bugs.
Think of it like this
A car's trip meter with four digits. It counts up to 9999, and one more kilometre makes it show 0000, not 10000. It didn't break; it simply has no room for a fifth digit. A Java int is a trip meter with 32 binary digits, and when it runs out of room it wraps around, except that the next value after the biggest positive number is the most negative one.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Overflow
- When a calculation's true result is too big (or too negative) to fit in the type, so the stored result is wrong.
- Wrap around
- Going past one end of the range and coming back at the other end, like a clock going from 12 to 1.
- Binary
- Writing numbers with only the digits 0 and 1. Each position is worth double the one to its right: 1, 2, 4, 8, 16, ...
- Two's complement
- The way Java (and almost every CPU) stores negative whole numbers: flip every bit of the positive number and add 1.
- Sign bit
- The leftmost bit of a signed integer. 1 means the number is negative.
- Widening
- Converting a value to a type with more bits, like
inttolong. It never loses information. - Exception
- An error signal thrown while a program runs. If nothing catches it, the program stops and prints what went wrong. Phase 7 covers them.
- BigInteger
- A class in
java.maththat stores whole numbers of any size, growing its memory as needed.
Step by step
01Counting in binary
Each binary digit (bit) is worth twice the one to its right. 00000101 is 4 + 1 = 5. 01111111 is 64+32+16+8+4+2+1 = 127, the largest value a byte can hold with the top bit 0.
Integer.toBinaryString(5) prints 101. You'll use it a lot in Topic 1.9.
02How negative numbers are stored: two's complement
To write -5 in a byte: start with 5 (00000101), flip every bit (11111010), add 1 (11111011). So 11111011 means -5.
Why this odd scheme? Because ordinary binary addition then just works for negative numbers too: 11111011 (-5) + 00000101 (5) = 1 00000000, and the ninth bit falls off, leaving 0. The CPU needs only one adder circuit for signed and unsigned numbers.
Another way to see it: in a byte the top bit is worth -128 instead of +128. 10000000 = -128, 11111111 = -128 + 127 = -1.
03Overflow is the bits running out
Add 1 to 127 in a byte: 01111111 + 1 = 10000000. The bit pattern is exactly what binary addition gives, but now the sign bit is set, so it reads as -128.
An int behaves the same with 32 bits: 2,147,483,647 + 1 = -2,147,483,648. The JVM's iadd instruction computes the true sum and keeps only the low 32 bits. No error, no warning.
04The number line is a circle
Picture all int values on a clock face. Counting up from 0 you reach MAX_VALUE, and one more step lands on MIN_VALUE, then you climb back towards -1 and 0. Subtracting 1 from MIN_VALUE jumps to MAX_VALUE.
This also explains why Math.abs(Integer.MIN_VALUE) is negative: the true answer, 2,147,483,648, is one more than MAX_VALUE, so it wraps back to MIN_VALUE.
05The widen-too-late trap
Java decides the type of an arithmetic expression from its operands, not from the variable you store into. In long x = 24 * 60 * 60 * 1000 * 1000; every operand is an int, so every multiplication is done in 32 bits. The true answer, 86,400,000,000, overflows to 500,654,080, and that wrong int is then widened into the long.
Fix it by making the first operand a long: 24L * 60 * 60 * 1000 * 1000. Evaluation goes left to right, so 24L * 60 is a long, and every later step stays long.
06Detect overflow with the exact methods
Math.addExact(a, b) returns a + b if it fits and otherwise throws ArithmeticException with the message integer overflow. The same family has subtractExact, multiplyExact, incrementExact, decrementExact, negateExact, and toIntExact for converting a long to an int safely.
They're nearly as fast as plain operators: HotSpot compiles them into a normal add followed by a check of the CPU's overflow flag. Use them where wrong numbers would be worse than a crash: money, quantities, sizes, array capacities.
Catching an exception uses try/catch, which Phase 7 teaches. For now, read the example as 'try this; if it throws, run the catch block'.
07When even long isn't enough: BigInteger
BigInteger from java.math stores a whole number in an array of ints that grows as needed, so it never overflows. The price: it's an object, every operation creates a new object, and it's much slower than a primitive.
You call methods instead of operators: a.add(b), a.multiply(b). Typical uses are cryptography, factorials of big numbers and exact combinatorics.
Try it yourself
- 1
Find where factorials break
Write
int f = 1;and multiply it by 2, 3, 4, ... printing each result. Predict where the numbers stop making sense (hint: 13! is 6,227,020,800). Then switchftolongand see how far you get (20! is the last one that fits). - 2
Swap in multiplyExact
In the 'famous bugs' example replace
24 * 60 * 60 * 1000 * 1000with nestedMath.multiplyExactcalls. Instead of a silently wrong number, you get an exception that points at the line. - 3
Predict the byte
Before running, predict what
byte b = (byte) 200;prints. Then run it. (200 is11001000in binary; with the top bit worth -128 that's -128 + 72 = -56.)
Code & diagrams
Expected output
max = 2147483647
max + 1 = -2147483648
min - 1 = 2147483647
byte 127+1 = -128
abs(min) = -2147483648
-min = -2147483648
binary of -5 as int: 11111111111111111111111111111011Expected output
micros per day, wrong: 500654080
micros per day, right: 86400000000
bad mid: -397483648
good mid: 1750000000
shift mid: 1750000000`try`/`catch` is explained in Phase 7. Here it just lets the program keep going after the error.
Expected output
addExact caught: integer overflow
toIntExact caught: integer overflow
long result: 2147483648
BigInteger: 92233720368547758070Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Let multiplyExact catch a real overflow
Compute 50,000 bytes per user times 100,000 users with Math.multiplyExact and don't catch the exception.
public class Main {
public static void main(String[] args) {
int bytesPerUser = 50_000;
int users = 100_000;
int total = Math.multiplyExact(bytesPerUser, users);
System.out.println(total);
}
}Break #2
Write a literal past the int range
Write int big = 2147483648;.
Myth vs fact
Myth
Java throws an error when an int overflows.
Fact
Plain +, -, * wrap silently. Only the Math.*Exact methods (and integer division by zero) throw.
Myth
Storing the result in a long prevents overflow.
Fact
The calculation's type comes from its operands. int * int overflows before being widened; make an operand long first.
Myth
Math.abs always returns a non-negative number.
Fact
Math.abs(Integer.MIN_VALUE) is Integer.MIN_VALUE. Java 15 added Math.absExact, which throws instead.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
JLS §15.18.2 and §15.17.1 define integer
+and*as producing the low-order bits of the mathematically exact result in two's complement, so overflow is fully specified behaviour in Java, unlike signed overflow in C/C++, which is undefined and lets compilers delete your overflow checks. - ▸
Math.addExactis implemented in Java asint r = x + y; if (((x ^ r) & (y ^ r)) < 0) throw ...: overflow happened exactly when both operands have the opposite sign to the result. HotSpot replaces it with an intrinsic using the CPU's overflow flag. - ▸
Unsigned arithmetic exists as methods since Java 8:
Integer.toUnsignedString,Integer.divideUnsigned,Integer.compareUnsigned,Integer.toUnsignedLong. Adding, subtracting and multiplying are the same bit operations for signed and unsigned, so only comparison, division and printing need helpers. - ▸
Hash codes are routinely computed with intentional overflow (
31 * h + cinString.hashCode), because wrapping arithmetic is fast and mixes bits well. Topic 9.4 shows how HashMap then spreads those bits.
Remember this
- 1
Java stores signed integers in two's complement. The top bit is the sign bit: 0 means zero or positive, 1 means negative. For a
byte,01111111is 127 and adding 1 gives10000000, which two's complement reads as -128. - 2
Overflow is silent. Java's
+,-,*onintandlongnever throw an exception and never print a warning; the extra high bits are simply discarded. The result is wrong but looks like a normal number. - 3
The classic trap is **multiplying in
intbefore widening**:long micros = 24 * 60 * 60 * 1000 * 1000;overflows inintarithmetic first and only then converts the wrong result tolong. Make the first operand along(24L * ...) so the whole calculation happens in 64 bits. - 4
The range is lopsided:
intgoes from -2,147,483,648 to 2,147,483,647, so the most negative number has no positive partner.Math.abs(Integer.MIN_VALUE)and-Integer.MIN_VALUEboth returnInteger.MIN_VALUE, still negative. - 5
To detect overflow use the exact methods added in Java 8:
Math.addExact,subtractExact,multiplyExact,incrementExact,negateExactandtoIntExact. They throwArithmeticException: integer overflowinstead of wrapping. To avoid it, use a wider type (long) orjava.math.BigInteger, which has no size limit. - 6
Overflow causes real bugs: the famous binary search bug
mid = (low + high) / 2sat in the JDK's ownArrays.binarySearchfor nine years until 2006. The fix,low + (high - low) / 2or(low + high) >>> 1, appears in the DSA course (/dsa) binary search module.
Explain it without notes
Explain two's complement and show how -1 is stored in a byte.
Why does long ms = 1000 * 60 * 60 * 24 * 365; give a wrong answer, and how do you fix it?
Why is Math.abs(Integer.MIN_VALUE) negative?
Why is (low + high) / 2 a bug in binary search, and what are two correct alternatives?
How would you guard a money calculation against overflow?
Practice
Compute 13! with an int and with a long and print both.
Average two ints, a = 2_000_000_000 and b = 2_000_000_000, the wrong way and the safe way.
Compute the number of seconds in 100 years (365 days each) as an int and as a long, and use Math.toIntExact to show that it doesn't fit in an int.
Trade-offs
- ↔
Wrapping arithmetic is the fastest possible and is exactly what hashing and checksums want, but it turns business-logic mistakes into silently wrong numbers.
- ↔
Math.*Exactcosts almost nothing and turns silent corruption into a loud failure; the trade is that you must decide what to do when it throws. - ↔
BigIntegercan never overflow but is far slower and allocates an object per operation; use it only when values truly exceedlong.
Done when you can
I can explain two's complement and convert a small negative number to binary.
I can predict the result of
Integer.MAX_VALUE + 1andMath.abs(Integer.MIN_VALUE).I spot the 'multiply in int, store in long' bug and fix it with an
L.I can use
Math.addExact,multiplyExactandtoIntExactto detect overflow.I can write an overflow-safe binary search midpoint.