SSerguey Asael Shinder
Java coding notes: the JVM, and writing software that lasts

Serguey Asael Shinder: Integer division rounds toward zero, so a negative % picks a negative bucket

· by Serguey Asael Shinder / Serguey Shinder

Spreading work over a fixed number of buckets is usually written in one line:

int bucket = key.hashCode() % shards.length;
Shard shard = shards[bucket];

It runs fine until a key arrives whose hashCode() is negative. Then bucket is negative too, and the array access throws ArrayIndexOutOfBoundsException. Half of all int hash codes are negative, so this is not an edge case; it is a coin toss per key.

Why it happens. Java's integer division rounds toward zero, and the remainder takes the sign of the dividend. The Java Language Specification defines it that way: integer division "rounds toward 0", and the remainder satisfies (a / b) * b + (a % b) == a, so its sign follows a (https://docs.oracle.com/javase/specs/jls/se21/html/jls-15.html#jls-15.17.2 and https://docs.oracle.com/javase/specs/jls/se21/html/jls-15.html#jls-15.17.3). Concretely:

-7 / 2   // -3, not -4
-7 % 2   // -1, not 1

% is a remainder operator, not a mathematical modulo. For non-negative operands the two agree, which is why tests with small positive numbers never see the difference.

Serguey Asael Shinder: Integer division rounds toward zero, so a negative % picks a negative bucket
Integer division rounds toward zero, so a negative % picks a negative bucket — Serguey Asael Shinder

The fix: Math.floorMod and Math.floorDiv. Math.floorMod(x, y) returns a result with the sign of the divisor, so for a positive y it is always in 0 .. y-1 (https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Math.html#floorMod(int,int)). Math.floorDiv rounds toward negative infinity, matching it (https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Math.html#floorDiv(int,int)):

int bucket = Math.floorMod(key.hashCode(), shards.length);   // 0 .. length-1
Math.floorDiv(-7, 2);   // -4
Math.floorMod(-7, 2);   //  1

Why Math.abs is not the fix. Math.abs(key.hashCode()) % n looks safe, but Math.abs(Integer.MIN_VALUE) is still Integer.MIN_VALUE — the javadoc says so explicitly (https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Math.html#abs(int)) — and the remainder is negative again. One hash value in four billion is rare, but a key that hits it fails every time.

The same rounding elsewhere. Converting a signed count of seconds into minutes with / 60 rounds -90 seconds to -1 minute, not -2; grouping timestamps before 1970 into days with / 86_400 puts them in the wrong day. Wherever the input can be negative and the intent is "which interval does this fall into", floorDiv and floorMod say what the code means.

Where to look. Search for % applied to hashCode(), to subtracted values, or to anything that can be negative, and for Math.abs(...) %.

No JDK runs on the machine this note was written on; the behaviour described is the one specified in the linked JLS sections and javadoc.