skip to content

What is a narrowing primitive conversion, why does it require an explicit cast, and what can go wrong?

level: middleimportance: must knowfreq 70%

answer

  1. Big -> small needs explicit cast
  2. Float->int: truncate toward zero, then clamp; NaN->0
  3. Int->int: drop high bits (modular, can go negative)
  4. (byte)300 == 44; (byte)128 == -128
  5. Constant that fits skips the cast; variable does not

basics

~20 s

Narrowing converts a bigger type into a smaller one, like double to int or int to byte. Java does not do it automatically because the value might not fit, so you must write a cast like (int)x, and you can lose data.

solid answer

~40 s

A narrowing primitive conversion goes from a larger type to a smaller one -- for example double to int, long to int, or int to byte. The target range cannot hold every source value, so the compiler refuses to do it silently; you must signal intent with an explicit cast such as (int) x. Two kinds of information loss can occur. For floating-to-integer, the fractional part is truncated toward zero (3.9 becomes 3, -3.9 becomes -3), and out-of-range values clamp to Integer.MAX_VALUE/MIN_VALUE, with NaN becoming 0. For integer-to-integer, the high-order bits are simply discarded, keeping only the low bits, which can flip the sign or produce a seemingly random value (e.g. (byte) 300 is 44). The cast is the developer's assertion that the loss is acceptable.

code

java · 10 lines
java
double d = 3.99;
int truncated = (int) d;          // 3  (truncates toward zero)
int big = (int) 1e20;             // 2147483647 (clamps to Integer.MAX_VALUE)
int nan = (int) Double.NaN;       // 0

int n = 300;
byte wrapped = (byte) n;          // 44 (low 8 bits kept, modular)

// safe, loud conversion:
int safe = Math.toIntExact(5_000_000_000L); // throws ArithmeticException

go deeper

for a junior

Knows narrowing needs a cast and can lose data, and can write (int) on a double.

for a middle

Distinguishes float-to-int truncation/clamping from int-to-int bit-dropping, and knows the constant-fits exception.

for a senior

Can compute results like (byte)300 from two's-complement, explains NaN->0 and clamping rules, and recommends Math.toIntExact / Math.round.

for a principal

Treats silent narrowing as an API/robustness hazard, advocates checked conversions and lint rules, and can reason about JLS conversion contexts across a codebase.

## Background: types and ranges Java's numeric primitives have fixed sizes: `byte` (8 bits), `short`/`char` (16), `int` (32), `long` (64), `float` (32), `double` (64). A type's **range** is the set of values it can represent. (See the widening question for the full table.) ## What narrowing is A **narrowing primitive conversion** converts a value to a type whose range is *smaller*, so the value might not fit. Examples: `double` -> `int`, `long` -> `int`, `int` -> `short`, `int` -> `byte`, `int` -> `char`. Because the source value may be too big, too small, or have a fractional part the target cannot store, the conversion is **lossy in general**. ## Why an explicit cast is required The Java compiler will **not** perform a narrowing conversion implicitly -- it would be too easy to silently corrupt data. You must write an **explicit cast**, putting the target type in parentheses before the value: ```java double d = 3.99; int i = (int) d; // i == 3 -- you asked for this ``` The cast is your signed statement: "I know this can lose information, and I accept it." ## Exactly what gets lost The loss depends on the kinds of types involved. ### A. Floating-point -> integer (e.g. double -> int, float -> long) Two steps, defined by the JLS: 1. **Truncate toward zero**: drop the fractional part. `3.9` -> `3`, `-3.9` -> `-3` (it does NOT round). 2. **Then fit into the integer type**: - If the truncated value fits, use it. - If it is too large, the result is `Integer.MAX_VALUE` (or the target's max); too small -> the target's min. - `NaN` (not-a-number) -> `0`. ```java System.out.println((int) 3.9); // 3 System.out.println((int) -3.9); // -3 System.out.println((int) 1e20); // 2147483647 (Integer.MAX_VALUE) System.out.println((int) Double.NaN); // 0 ``` ### B. Integer -> smaller integer (e.g. int -> byte, long -> int) Java simply **discards the high-order bits** and keeps the low-order bits that fit in the target. Because the target is signed two's-complement, the remaining top bit becomes the sign bit, so the result can be negative or look random: ```java System.out.println((byte) 300); // 44 (300 = 0b1_0010_1100; keep low 8 bits = 0010_1100 = 44) System.out.println((byte) 128); // -128 (low 8 bits 1000_0000 = -128 in two's complement) System.out.println((int) 4294967296L); // 0 (low 32 bits of 2^32 are all zero) ``` This is **modular arithmetic** (value mod 2^bits, reinterpreted as signed), not clamping. That difference from case A is a classic interview trap. ## Two narrowing-then-widening combos Some conversions are defined as a narrowing followed by a widening: `byte` -> `char` first widens byte to int conceptually, but the JLS classifies `byte`->`char` as narrowing-and-widening because char is unsigned. Practically, you still need a cast. ## Where narrowing also appears: compile-time constants There is a special rule: assigning a *constant expression* that fits the target needs **no** cast, even though int->byte is normally narrowing: ```java byte b = 100; // OK: 100 is a constant that fits in byte byte c = 200; // COMPILE ERROR: 200 does not fit in byte int i = 100; byte d = i; // ERROR: i is a variable, not a constant -> cast needed ``` This is the **assignment context / constant narrowing** exception and is heavily tested. ## Practical guidance - Only cast down when you know the range is safe, or you explicitly want truncation/wrap. - Use `Math.round`/`Math.floor` if you want rounding rather than truncation. - Use `Math.toIntExact(long)` to convert long->int with an exception on overflow instead of silent wrap.

  • Why does (byte) 130 give -126?
    Only the low 8 bits are kept. 130 = 1000_0010. As signed two's complement that bit pattern is -126. Integer-to-byte narrowing discards high bits rather than clamping.
  • Why does byte b = 100; compile but byte b = 300; does not, while int i=100; byte b=i; also fails?
    100 is a constant expression that fits in byte, so the assignment-context constant rule allows it without a cast. 300 does not fit, so it errors. i is a variable (not a compile-time constant), so the compiler cannot prove it fits and demands an explicit cast.
  • How can I convert a long to an int but fail loudly on overflow?
    Use Math.toIntExact(long), which throws ArithmeticException if the value does not fit, instead of the silent bit-dropping a plain cast does.

saying these in an interview costs you the question

  • Saying narrowing rounds floating values (it truncates toward zero)
  • Saying int-to-byte clamps to byte max (it wraps / drops high bits)
  • Claiming any int can be assigned to byte without a cast (only fitting constants can)
  • Forgetting NaN to int becomes 0, not an exception

context