skip to content
everything.basics.castingAndOverflow

casting and overflow

widening, narrowing, and why Integer.MAX_VALUE + 1 is negative.

chapter
chapter 02.03
reading time
7 min read

java converts a smaller type to a bigger one for you (widening), and makes you write a cast to go the other way (narrowing), because narrowing can lose information. integer math that doesn't fit doesn't fail, it wraps around: Integer.MAX_VALUE + 1 is Integer.MIN_VALUE, silently.

the picture

pouring water between glasses. a small glass into a big one always fits, so nobody needs to ask (widening). a big glass into a small one might spill, so java makes you say "yes, i know, pour anyway" out loud, and that's what a cast is (narrowing). whatever doesn't fit is gone.

the three widenings that can lose precision (no cast, no warning):

pick one to see it round.

arrows point the way java converts automatically. going against an arrow needs a cast. the dashed ones are allowed without a cast but can round: pick one to see it happen.

widening: automatic

any arrow in that chain happens on its own: in assignments, method calls, and mixed arithmetic.

java
int count = 42;
long big = count;          // int → long, fine
double avg = count / 5.0;  // count is widened to double before dividing: 8.4
int code = 'A' + 1;        // char → int: 66

it's called "widening" because the target can hold every value of the source. well, almost: a float has only 24 bits of precision and an int has 32, so big ints get rounded on the way into a float, and big longs on the way into a float or double. no cast, no warning. that's the dashed arrows above.

narrowing: you have to ask

Main.java
void main() {
    IO.println((int) 3.99 + " " + (int) -3.99 + " " + (int) 1e20 + " " + (int) Double.NaN);
    IO.println((byte) 200 + " " + (byte) 300 + " " + (char) 65 + " " + (short) 70000 + " " + (int) 3_000_000_000L);
}
output
3 -3 2147483647 0
-56 44 A 4464 -1294967296

two completely different rules hiding in there:

  • decimal → whole number truncates toward zero (3.99 → 3, -3.99 → -3), and if the value is too big it saturates at the type's max or min: (int) 1e20 is 2,147,483,647. NaN becomes 0. if you wanted rounding, use Math.round.
  • whole number → smaller whole number just keeps the low bits and throws the rest away. no saturation, no rounding. that's how 200 turns into -56.
int x =
cast to =
00000000
00000000
00000000
11001000
← 24 bits dropped
(byte) 200 = -56

keep the low 8 bits. the leftmost kept bit is a 1, and in a byte that's the sign bit, so it reads as negative.

type an int (or pick one), choose the target type. the struck-out bits are dropped. the amber bit is the one that becomes the sign bit of the smaller type, which is why 200 comes out negative.
predict the output
java
int x = 300;
byte b = (byte) x;
IO.println(b);

overflow: the odometer

an int isn't a number line that goes on forever. it's a circle of 2³² values. counting up from 0 you go clockwise through the positives to MAX_VALUE, and one more step lands on MIN_VALUE, the most negative int, and you keep counting up from there through the negatives back to 0. like a car's odometer rolling over from 99999 to 00000, except the other side of the dial is negative.

type =
064MAX | MIN-64positivenegative
byte value
120

press + 1 a few times.

byte 120, in bits1200x78
sign · 1 bitvalue · 7 bits
press + 1 near the bottom of the dial. the needle crosses the red line between MAX and MIN and just keeps going. start with byte (256 values, you can see every tick), then try int. the bits below are the real ones.
Main.java
void main() {
    IO.println(Integer.MAX_VALUE + 1);
    int big = 2_000_000_000;
    IO.println(big + big);
    IO.println(big + big > 0);
}
output
-2147483648
-294967296
false

no exception, no warning, no NaN. the CPU adds the bits and drops the carry, and java defines that as correct. it's fast, and it's also how you get a negative account balance after a big deposit.

the circle is lopsided: there's one more negative value than positive, because 0 takes a spot on the positive side. so -Integer.MIN_VALUE has nowhere to go and wraps back to itself.

catching it when it matters

when an overflow would be a bug (money, sizes, counters that must never go negative), ask for an exception instead of a wrap. Math has exact versions of the risky operations:

Main.java
void main() {
    IO.println("adding...");
    int total = Math.addExact(Integer.MAX_VALUE, 1);
    IO.println(total);
}
output
adding...
Exception in thread "main" java.lang.ArithmeticException: integer overflow

addExact, subtractExact, multiplyExact, incrementExact, negateExact (for int and long), and toIntExact for a long that has to fit in an int. they cost about nothing, and they turn a silent wrong number into a loud stack trace. for numbers with no upper limit at all, there's BigInteger (see math and random numbers).

the sneaky cast in +=

compound assignment casts the result back to the variable's type for you. the plain version doesn't:

Main.java
void main() {
    byte b = 10;
    b += 300;          // b = (byte) (b + 300)
    char c = 'a';
    c += 1;            // c = (char) (c + 1)
    IO.println(b + " " + c);
}
output
54 b

b = b + 300 wouldn't even compile (error: incompatible types: possible lossy conversion from int to byte), because b + 300 is an int. but b += 300 quietly narrows 310 to 54. convenient for chars and counters, surprising everywhere else.

one level deeper:why two's complement+

to negate a number: flip every bit, then add 1. so -1 is all 32 bits set, and -2147483648 is a 1 followed by 31 zeros. the payoff is that the CPU doesn't need separate circuits for signed and unsigned addition: it adds the bit patterns the same way either way, and the wrap-around falls out for free. the cost is the lopsided range (−2³¹ to 2³¹ − 1) and the fact that the top bit doubles as the sign. flip the leftmost bit in any of the bit figures on this page and watch the number jump by 2³¹. more about how the integer types are stored in variables and primitives.

keylines
  • small → big is automatic (widening). big → small needs a cast (narrowing).
  • narrowing a whole number keeps the low bits. decimal → int truncates, and saturates when too big.
  • int and long math wraps around silently. MAX_VALUE + 1 == MIN_VALUE.
  • a long on the left doesn't make the right side long math. put an L on the first literal.
  • when overflow would be a bug, use Math.addExact and friends. they throw instead of lying.