JAVA / VARIABLES, PRIMITIVES AND TYPES
Integer division, remainder and overflow traps
Predict integer / and % results for negative operands, and spot the int expressions that silently wrap past 2147483647.
What you will learn
- Predict / and % for negative operands using the identity (a/b)*b + a%b == a
- Use Math.floorDiv and Math.floorMod for floor semantics and non-negative results
- Force long arithmetic with an L literal before a product can pass 2147483647
- Detect overflow instead of ignoring it using Math.addExact and Math.multiplyExact
Understanding Integer division, remainder and overflow traps
When both operands of / are integer types, Java produces an integer: the exact quotient is truncated toward zero, so 7 / 2 is 3 and -7 / 2 is -3. Nothing is rounded and nothing is promoted to double, because the result type of an arithmetic expression comes from its operands and never from what you assign it to. The % operator returns whatever truncation discarded, and it is defined so that (a / b) * b + a % b reconstructs a exactly.
That identity forces % to take the sign of the dividend, which is why -7 % 2 is -1 rather than the 1 a mathematical modulo would give. The consequence bites in two common places: n % 2 == 1 misses every negative odd number, and array[i % length] throws when i is negative. Math.floorDiv and Math.floorMod round the quotient toward negative infinity instead, so floorDiv(-7, 2) is -4 and floorMod(-7, 2) is 1, which stays non-negative for any positive divisor.
Range is the second trap. An int is 32 bits of two's complement, so every int operation is arithmetic modulo 2^32, and crossing 2147483647 wraps to a large negative number with no warning. Because the expression type is fixed before the assignment is considered, long ms = 1000 * 60 * 60 * 24 * 365 does the whole product in int and stores the already-wrapped 1471228928; writing 1000L as the first factor promotes the entire chain to 64 bits. Division and remainder by zero are the only integer operations that throw rather than wrap, and even Integer.MIN_VALUE / -1 stays silent by returning MIN_VALUE.
public class IntegerTraps {
public static void main(String[] args) {
System.out.println("7 / 2 = " + (7 / 2));
System.out.println("-7 / 2 = " + (-7 / 2));
System.out.println("7 % 2 = " + (7 % 2));
System.out.println("-7 % 2 = " + (-7 % 2));
System.out.println("rebuilt = " + ((-7 / 2) * 2 + (-7 % 2)));
System.out.println("floorDiv = " + Math.floorDiv(-7, 2));
System.out.println("floorMod = " + Math.floorMod(-7, 2));
int max = Integer.MAX_VALUE;
System.out.println("max = " + max);
System.out.println("max + 1 = " + (max + 1));
long yearMs = 1000 * 60 * 60 * 24 * 365;
System.out.println("int math = " + yearMs);
System.out.println("long math = " + 1000L * 60 * 60 * 24 * 365);
}
}Integer / and % truncate toward zero and their result is wrapped into the bit width of the operands, which is chosen before the compiler ever looks at the target variable.
Worked examples
Zero divisors behave differently for int and double
Integer division by zero throws, while the same expression in floating point returns Infinity or NaN.
public class DivisionByZero {
public static void main(String[] args) {
int a = 10;
int b = 0;
System.out.println("10 / 3 : " + (a / 3));
System.out.println("10.0 / 0 : " + (10.0 / b));
System.out.println("0.0 / 0.0 : " + (0.0 / b));
try {
System.out.println(a / b);
} catch (ArithmeticException e) {
System.out.println("int / 0 : " + e.getMessage());
}
try {
System.out.println(a % b);
} catch (ArithmeticException e) {
System.out.println("int % 0 : threw " + e.getClass().getSimpleName());
}
}
}Example explained
Line 1a / 3 is 3 and not 3.333 because both operands are int, so the fractional part has nowhere to live.
Line 210.0 / b promotes b to 0.0 and returns Infinity, since IEEE 754 doubles have a bit pattern for it.
Line 3int has no such value, so the JVM raises ArithmeticException with the message / by zero.
Line 4a % b throws as well, because the remainder is defined from a quotient that does not exist.
Wraparound versus a checked operation
Overflow is silent by default, Math.addExact turns it into an exception, and Integer.MIN_VALUE has no positive counterpart.
public class OverflowChecks {
public static void main(String[] args) {
int big = 2_000_000_000;
System.out.println("wrapped : " + (big + big));
try {
System.out.println(Math.addExact(big, big));
} catch (ArithmeticException e) {
System.out.println("addExact : " + e.getMessage());
}
int min = Integer.MIN_VALUE;
System.out.println("min / -1 : " + (min / -1));
System.out.println("Math.abs : " + Math.abs(min));
System.out.println("-min : " + (-min));
}
}Example explained
Line 1big + big is 4000000000, exactly 4294967296 too large, so it wraps around to -294967296.
Line 2Math.addExact computes the same sum but inspects the sign bits and throws instead of returning a wrong answer.
Line 3min / -1 should be 2147483648, which does not fit in an int, so the language specification says the result is the dividend itself.
Line 4Math.abs(min) and -min fail identically, which is why abs is not a safe way to force a value non-negative.
Midpoints and wrapping indexes
Two real bugs caused by an overflowing sum and by a negative remainder.
public class MidpointAndWrap {
public static void main(String[] args) {
int low = 1_500_000_000;
int high = 2_000_000_000;
System.out.println("naive midpoint : " + ((low + high) / 2));
System.out.println("safe midpoint : " + (low + (high - low) / 2));
int[] slots = {10, 20, 30};
int step = -1;
System.out.println("step % 3 : " + (step % slots.length));
System.out.println("floorMod : " + Math.floorMod(step, slots.length));
try {
System.out.println(slots[step % slots.length]);
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("bad index : threw " + e.getClass().getSimpleName());
}
System.out.println("good index : " + slots[Math.floorMod(step, slots.length)]);
}
}Example explained
Line 1low + high is 3500000000, which wraps negative before the division runs, so the midpoint lands far outside the range.
Line 2low + (high - low) / 2 only ever computes a difference that fits in an int, so the intermediate value never overflows.
Line 3step % slots.length is -1 because % keeps the sign of the dividend, and a negative index throws rather than wrapping to the end.
Line 4Math.floorMod(-1, 3) is 2, the index of the last element, which is what wrap-around code actually wants.
Important notes
Integer.MIN_VALUE / -1, Math.abs(Integer.MIN_VALUE) and -Integer.MIN_VALUE all return Integer.MIN_VALUE, which is the one negative int with no positive twin.
long wraps the same way, just at 9223372036854775807; switching int to long moves the boundary but never removes it.
Common mistakes
Writing long ms = 1000 * 60 * 60 * 24 * 365 and assuming the long target protects the product; all factors are int, so 1471228928 is stored.
Testing for odd with n % 2 == 1: for n = -3 the remainder is -1, so every negative odd number is classified as even.
Computing a percentage as done / total * 100 with int operands: the division truncates to 0 first, so the result is only ever 0 or 100.
Try it yourself
Change, predict, then run
Print a / b, a % b, Math.floorDiv(a, b) and Math.floorMod(a, b) for the four pairs (7,2), (-7,2), (7,-2) and (-7,-2). Then state which rows have identical quotients and explain what the sign of the operands has to do with it.
Open the Java workspaceCheck your understanding
Why does long total = 24 * 60 * 60 * 1000 * 365; store 1471228928 instead of 31536000000?
- Every factor is an int literal, so the product is computed in 32-bit arithmetic and wraps before the widening to long happens
- long cannot hold values above 2147483647 unless you use BigInteger
- Java rounds the product to the nearest value the multiplication hardware can represent
- The multiplications happen in long, but the assignment narrows the result back to 32 bits
Show answer
The type of an arithmetic expression is fixed by its operands, so all five factors are int and each intermediate product wraps modulo 2^32; the widening to long happens afterwards, on a value that is already wrong. Option 4 reverses the order, and it is also impossible: Java never implicitly narrows on assignment. Writing 24L as the first factor promotes the whole chain and gives 31536000000.