Type casting and numeric promotion
Widening, narrowing and the promotion rules that decide the type of every arithmetic expression, plus the overflow and truncation bugs they cause.
On this page
🔀 What are type casting and numeric promotion?
Casting converts a value from one type to another. Going to a bigger type (int → long) is widening and happens automatically; going to a smaller one (double → int) is narrowing and needs an explicit cast like (int) 3.99, which gives 3. Numeric promotion is the set of rules Java applies before arithmetic: in byte + byte, both sides are promoted to int first, so the result is an int.
For this lesson, we'll focus on primitive conversions: which ones are automatic, what narrowing does to your bits, and the promotion rules that decide the type of every arithmetic expression. Interviewers lean on this topic for "what does this print?" questions, because three lines of arithmetic can hide an overflow, a truncation and a compile error.
Below the line (out of scope)
- Reference casts,
instanceofandClassCastException(Core Java module) - Boxing conversions (see Wrapper classes and autoboxing)
BigDecimalandBigIntegerarithmetic
The 30-second answer
Widening (byte → short → int → long → float → double, and char → int) is automatic, but int/long → float and long → double can silently lose precision. Narrowing needs a cast and keeps only the low bits for integers, while double → int truncates toward zero and clamps at Integer.MIN_VALUE/MAX_VALUE. Before arithmetic, Java applies binary numeric promotion (JLS §5.6): if either operand is double the maths is double, else float, else long, otherwise everything is int. That's why byte b = b + 1 doesn't compile while b += 1 does, and why 24 * 60 * 60 * 1000 * 1000 overflows before it ever reaches a long.
What you’ll learn
- Which conversions are widening (automatic) and which are narrowing (need a cast), and which widenings lose precision
- What a cast does to the bits: low-bit truncation for integers, truncate and clamp for
double→int - The binary numeric promotion ladder (JLS §5.6) and why
byte b = b + 1doesn't compile butb += 1does - Why
24 * 60 * 60 * 1000 * 1000overflows before it reaches along, and how to fix it - How
charbehaves in arithmetic and string concatenation
01Picture it first: the widening graph
Picture a set of one-way streets. You can always drive from a narrower type to a wider one without a ticket (no cast). Driving the other way is allowed, but only if you sign a waiver: the cast, which says "I accept that data may be lost".
Widening conversions: byte to short to int to long to float to double, char to int; int to float, long to float and long to double may lose precision
Notice two things about the graph:
charsits on a side road. It widens toint, butbyte→char,short→charandchar→shortall need casts, becausecharis unsigned andshortis signed: neither range contains the other.- "Widening" doesn't mean "lossless". A
floathas only 24 bits of precision, so(float) 16_777_217becomes16777216.0. Adoublehas 53, so alongabove 2⁵³ can come back different:(long) (double) 9_007_199_254_740_993Lgives9007199254740992.
02Narrowing: what a cast actually does
Try it yourself first
Before reading on, predict each value.
System.out.println((int) 3.99); // ?
System.out.println((int) -3.99); // ?
System.out.println((int) 1e20); // ?
System.out.println((int) Double.NaN);// ?
System.out.println((byte) 200); // ?
System.out.println((byte) 300.7); // ?
System.out.println((short) 70000); // ?
3
-3
2147483647
0
-56
44
4464
There are two different rules at work, and mixing them up is where people slip:
- Integer → smaller integer keeps only the low bits.
200is1100 1000in binary; as abyte, the top bit is the sign bit, so you get -56.70000keeps its low 16 bits and becomes4464. Nothing is clamped, the high bits are simply thrown away. - Floating point → integer truncates toward zero (not floor:
-3.99becomes-3), mapsNaNto0, and clamps values that are too large toMAX_VALUEorMIN_VALUE. When the target isbyte,shortorchar, Java first converts tointwith those rules and then keeps the low bits. That's why(byte) 300.7is(byte) 300, which is44.
Casting is not rounding. If you want "nearest", use Math.round(x) (half up: Math.round(2.5) is 3, Math.round(-2.5) is -2), and if you want to fail on overflow, use Math.toIntExact(longValue).
03Binary numeric promotion (JLS §5.6)
Every arithmetic operator (+ - * / %, comparisons, bitwise & | ^ on numbers, and the conditional ?: with numeric operands) first promotes both operands to a common type. The rule is a short ladder, checked top to bottom:
| If either operand is... | both become... |
|---|---|
double |
double |
otherwise float |
float |
otherwise long |
long |
otherwise (byte, short, char, int) |
int |
There's also unary promotion: byte, short and char become int for unary -, +, ~, array indices and each operand of a shift. The upshot is that Java never does arithmetic in byte, short or char. The smallest arithmetic type is int.
byte b = b + 1 vs b += 1
Try it yourself first
Which of these lines compile?
byte b = 10;
b = b + 1; // (1)
b += 1; // (2)
b++; // (3)
byte c = 10 + 1;// (4)
Line (1) is a compile error: incompatible types: possible lossy conversion from int to byte. b + 1 is an int, and you can't assign an int expression to a byte without a cast. Lines (2), (3) and (4) compile:
- Compound assignment
E1 op= E2is defined asE1 = (T) (E1 op E2): the cast toE1's type is built in. Sob += 1compiles, and withb = 127it silently gives-128. The same hidden cast makesint i = 10; i += 3.7;legal, leavingiat13. ++and--also include the narrowing cast.10 + 1is a constant expression. If its value fits in the target type, the compiler narrows it for you.byte c = 128;fails because 128 doesn't fit.
Integer division
When both operands are integers, / is integer division that truncates toward zero, and the type of the variable you assign to doesn't change that.
int sum = 7, count = 2;
double avg = sum / count; // 3.0: divided as int, then widened
double ok = (double) sum / count; // 3.5: cast binds tighter than /
double bad = (double) (sum / count); // 3.0: cast applied too late
System.out.println(5 / 2 * 2.0); // 4.0: 5 / 2 is already 2
System.out.println(2.0 * 5 / 2); // 5.0: double from the first step
Evaluation is left to right with normal precedence, and promotion happens per operation, not once for the whole line. That's the root of the next bug too.
04Long overflow before widening
Try it yourself first
What does this print, and how would you fix it?
long microsPerDay = 24 * 60 * 60 * 1000 * 1000;
System.out.println(microsPerDay);
It prints 500654080. Every literal is an int, so every multiplication is int maths. The true value, 86,400,000,000, overflows int at the last step, wraps, and only then is the wrapped result widened to long. The variable's type comes too late to help.
long microsPerDay = 24L * 60 * 60 * 1000 * 1000; // 86400000000
Making the first operand a long promotes every following step to long. Putting the L on the last literal happens to work here, because 24 * 60 * 60 * 1000 (86,400,000) still fits in an int, but it's fragile: in 30 * 24 * 60 * 60 * 1000 * 1000L the int part has already overflowed before the long shows up. The habit to build is "widest operand first", or Math.multiplyExact so overflow throws.
I've seen this exact bug in cache TTLs and token expiry: long ttl = 30 * 24 * 60 * 60 * 1000; (30 days in ms) silently becomes -1702967296. A negative TTL is often treated as "expire immediately", so the cache just stops working, with no exception anywhere. Duration.ofDays(30).toMillis() avoids the whole class of bug.
05char arithmetic
A char is a 16-bit unsigned number, so it takes part in arithmetic like any integer, after promotion to int.
char c = 'a';
System.out.println(c + 1); // 98 (int)
System.out.println((char) (c + 1)); // b
c += 1; // OK: compound assignment casts back to char
char d = 'a' + 1; // OK: constant expression that fits
// char e = c + 1; // compile error: int to char
System.out.println('a' + 'b' + "c");// 195c (left to right: int addition first)
System.out.println("c" + 'a' + 'b');// cab (string concatenation from the start)
The last two lines are a favourite: + is evaluated left to right, and whether it means "add" or "concatenate" depends on whether a String has appeared yet.
06Advanced: edge cases worth knowing
Many candidates are quick to say "widening is always safe because it goes to a bigger type". It's frequently a mistake because long → double and int → float are widening yet lossy. A long ID (for example a Snowflake-style 64-bit ID) passed through a double, or through JSON parsed by a JavaScript client that only has doubles, loses its last digits above 2⁵³ (about 9 × 10¹⁵). Keep IDs as long or String end to end.
A few more rules that show up in "what does this print?" questions:
- Shift distance is masked. For
int, only the low 5 bits of the distance are used, so1 << 32is1, not0. Forlongit's 6 bits:1L << 32is4294967296. - The conditional operator promotes.
true ? 1 : 2.0is thedouble1.0, because the two branches undergo binary numeric promotion. floatliterals needf.float f = 1.5;doesn't compile:1.5is adouble, anddouble→floatis narrowing.(float) 0.1widened back is not0.1.(double) 0.1fis0.10000000149011612, which is why0.1f == 0.1isfalse.- Overflow and precision checks exist.
Math.toIntExact,Math.multiplyExact,Math.addExactthrow instead of wrapping.
Primitive conversions are close to free: widening is usually a single sign-extension or move instruction, and double → int is one CPU instruction plus a fix-up for NaN and out-of-range values. The cost of casting is correctness, not speed. Don't avoid a (long) cast for performance reasons.
07How to say it in the interview
When I ask about casting, I'm listening for three things: do you know that narrowing drops high bits (and doesn't clamp) for integers, do you know arithmetic happens in at least int, and can you explain why 24 * 60 * 60 * 1000 * 1000 goes wrong instead of just knowing that it does.
In the interview
"Widening conversions like int to long are automatic, though int or long to float and long to double can lose precision. Narrowing needs a cast: integer narrowing keeps only the low bits, and double to int truncates toward zero and clamps. Before any arithmetic, Java promotes both operands: to double, else float, else long, otherwise int. So byte plus byte is an int, which is why b = b + 1 needs a cast while b += 1 has one built in. And because promotion happens per operation, an all-int expression can overflow before it's assigned to a long, so I put the long operand first or use Math.multiplyExact."
Here's how this might look if you sketch it on the whiteboard:
On the whiteboard
Conversions
- widen: byte→short→int→long→float→double, char→int
- lossy widening: int→float, long→float, long→double
- narrow int: keep low bits ((byte)200 = -56)
- narrow double→int: truncate, NaN→0, clamp
- promotion: double > float > long > int (min int)
b += 1=b = (byte)(b + 1)24L * 60 * ...: long first