Course contents

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.

FreeLesson11 min readCoreReviewed Oct 2026 · Java 21
On this page
  1. Picture it first: the widening graph
  2. Narrowing: what a cast actually does
  3. Binary numeric promotion (JLS §5.6)
  4. Long overflow before widening
  5. char arithmetic
  6. Advanced: edge cases worth knowing
  7. How to say it in the interview

🔀 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, instanceof and ClassCastException (Core Java module)
  • Boxing conversions (see Wrapper classes and autoboxing)
  • BigDecimal and BigInteger arithmetic

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 + 1 doesn't compile but b += 1 does
  • Why 24 * 60 * 60 * 1000 * 1000 overflows before it reaches a long, and how to fix it
  • How char behaves 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".

Fig. 1

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

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:

  1. char sits on a side road. It widens to int, but byte → char, short → char and char → short all need casts, because char is unsigned and short is signed: neither range contains the other.
  2. "Widening" doesn't mean "lossless". A float has only 24 bits of precision, so (float) 16_777_217 becomes 16777216.0. A double has 53, so a long above 2⁵³ can come back different: (long) (double) 9_007_199_254_740_993L gives 9007199254740992.

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. 200 is 1100 1000 in binary; as a byte, the top bit is the sign bit, so you get -56. 70000 keeps its low 16 bits and becomes 4464. Nothing is clamped, the high bits are simply thrown away.
  • Floating point → integer truncates toward zero (not floor: -3.99 becomes -3), maps NaN to 0, and clamps values that are too large to MAX_VALUE or MIN_VALUE. When the target is byte, short or char, Java first converts to int with those rules and then keeps the low bits. That's why (byte) 300.7 is (byte) 300, which is 44.
Tip

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= E2 is defined as E1 = (T) (E1 op E2): the cast to E1's type is built in. So b += 1 compiles, and with b = 127 it silently gives -128. The same hidden cast makes int i = 10; i += 3.7; legal, leaving i at 13.
  • ++ and -- also include the narrowing cast.
  • 10 + 1 is 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.

In the real world

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

Where candidates slip

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, so 1 << 32 is 1, not 0. For long it's 6 bits: 1L << 32 is 4294967296.
  • The conditional operator promotes. true ? 1 : 2.0 is the double 1.0, because the two branches undergo binary numeric promotion.
  • float literals need f. float f = 1.5; doesn't compile: 1.5 is a double, and double → float is narrowing.
  • (float) 0.1 widened back is not 0.1. (double) 0.1f is 0.10000000149011612, which is why 0.1f == 0.1 is false.
  • Overflow and precision checks exist. Math.toIntExact, Math.multiplyExact, Math.addExact throw instead of wrapping.
Performance angle

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
Practise this topicData types & variables: interview questions & scenarios
Further reading (5)