☕ Java

Narrowing Casting

Narrowing casting converts a larger data type into a smaller one. Unlike widening, it's never automatic — you must write the cast explicitly. And unlike widening, it can silently corrupt your data. No exception, no warning — just wrong values. Here's exactly how narrowing works, where it loses data, and how to do it safely.

What Is Narrowing Casting?

Narrowing casting — also called explicit casting or narrowing conversion — converts a value from a larger type to a smaller type. It's the opposite of widening. Where widening is always safe, narrowing is always potentially lossy: the smaller type may not be able to represent the full range or precision of the original value. Java forces you to write narrowing casts explicitly. The syntax — (targetType) before the value — is Java's way of making you acknowledge: "I know this conversion might lose data, and I'm doing it deliberately." The two things narrowing can lose: - Magnitude — if the value is too large for the target type, high-order bits are silently truncated - Precision — if a floating-point value is cast to an integer, the decimal part is discarded

Narrowing Syntax and the Hierarchy

The narrowing hierarchy is the reverse of widening. Any conversion that goes right-to-left requires an explicit cast:
Java
// Narrowing hierarchy (right to left requires explicit cast):
// doublefloatlongintshortbyte
//                          ↓
//                         char

// Syntax: (targetType) expression
double d = 9.99;
float  f = (float)  d;   // doublefloat
long   l = (long)   d;   // doublelong
int    i = (int)    d;   // doubleint
short  s = (short)  d;   // doubleshort
byte   b = (byte)   d;   // doublebyte

// You can skip steps — narrowing works across multiple levels:
double big = 1234.99;
byte small = (byte) big;  // double directly to byte — valid (but very lossy)

// Without the cast — compile error:
double x = 3.14;
int y = x;         // COMPILE ERROR: possible lossy conversion from double to int
int z = (int) x;   // correct — explicit cast, z = 3

Decimal Truncation — double and float to Integer Types

When you cast a floating-point value to any integer type, the decimal part is always discarded — truncated toward zero, not rounded. This is one of the most common sources of subtle bugs.
Java
// Truncation — NOT rounding:
double d1 = 9.1;
double d2 = 9.5;
double d3 = 9.9;
double d4 = -9.9;

System.out.println((int) d1);   // 9  — truncated
System.out.println((int) d2);   // 9  — truncated, NOT rounded to 10
System.out.println((int) d3);   // 9  — truncated, NOT rounded to 10
System.out.println((int) d4);   // -9 — truncated toward zero, NOT -10

// If you need rounding, use Math.round() before casting:
System.out.println(Math.round(9.5));    // 10 — rounds to nearest
System.out.println(Math.round(9.4));    // 9
System.out.println(Math.round(-9.5));   // -9 — rounds toward positive infinity
System.out.println((int) Math.round(9.9));  // 10 — round then cast to int

// Math.floor and Math.ceil for directed rounding:
System.out.println((int) Math.floor(9.9));  // 9  — round down
System.out.println((int) Math.ceil(9.1));   // 10 — round up

// float to int — same truncation behavior:
float f = 99.99f;
int fromFloat = (int) f;
System.out.println(fromFloat);  // 99 — decimal truncated

Overflow — When the Value Doesn't Fit

When you narrow an integer type and the value is outside the target type's range, the high-order bits are silently discarded. The result wraps around — producing a completely different value with no warning or exception.
Java
// int to bytebyte range is -128 to 127:
int a = 127;
int b = 128;
int c = 255;
int d = 256;
int e = 300;

System.out.println((byte) a);   //  127 — fits exactly
System.out.println((byte) b);   // -128 — just outside range, wraps to minimum
System.out.println((byte) c);   //   -1 — wraps around
System.out.println((byte) d);   //    0256 wraps to 0
System.out.println((byte) e);   //   44300 - 256 = 44

// How wrapping works — only the lowest 8 bits are kept for byte:
// 300 in binary: 0000_0001_0010_1100
// Keep 8 bits:        0010_1100  = 44

// int to shortshort range is -32768 to 32767:
int big = 40000;
System.out.println((short) big);   // -25536 — wrapped

// long to int — high 32 bits are discarded:
long huge = 10_000_000_000L;       // 10 billion
System.out.println((int) huge);    // 1410065408 — corrupted

long safe = 2_000_000_000L;        // 2 billion — within int range
System.out.println((int) safe);    // 2000000000 — correct

// Checking before casting to avoid overflow:
long value = 10_000_000_000L;
if (value >= Integer.MIN_VALUE && value <= Integer.MAX_VALUE) {
    int intValue = (int) value;   // safe
} else {
    System.out.println("Value too large for int");
}

// Math.toIntExact() — throws ArithmeticException instead of silently overflowing:
int exact = Math.toIntExact(2_000_000_000L);   // 2000000000 — fine
int overflow = Math.toIntExact(10_000_000_000L); // throws ArithmeticException

Narrowing Between Floating-Point Types

Narrowing from double to float loses precision — float has roughly half the significant digits of double. The value may be rounded to the nearest representable float, which can differ from the original double value.
Java
// double to float — precision loss:
double d1 = 3.141592653589793;   // 15-16 significant digits
float  f1 = (float) d1;
System.out.println(d1);  // 3.141592653589793
System.out.println(f1);  // 3.1415927 — rounded to 7 significant digits

double d2 = 123_456_789.987654321;
float  f2 = (float) d2;
System.out.println(d2);  // 1.2345678998765432E8
System.out.println(f2);  // 1.23456792E8 — last digits differ

// Special values survive narrowing:
double inf = Double.POSITIVE_INFINITY;
float  fInf = (float) inf;
System.out.println(fInf);  // Infinity — preserved

double nan = Double.NaN;
float  fNan = (float) nan;
System.out.println(fNan);  // NaN — preserved

// Very small double values may underflow to 0.0f:
double tiny = Double.MIN_VALUE;   // ~5E-324
float  fTiny = (float) tiny;
System.out.println(fTiny);        // 0.0 — underflows to zero

// Very large double values may overflow to Infinity in float:
double huge = Double.MAX_VALUE;   // ~1.8E308
float  fHuge = (float) huge;
System.out.println(fHuge);        // Infinity — overflows float's max (~3.4E38)

Narrowing with char

char is a 16-bit unsigned type (0 to 65,535). Narrowing to and from char has some unique behavior because it's the only unsigned primitive in Java.
Java
// int to char — keeps lower 16 bits:
int code = 65;
char letter = (char) code;
System.out.println(letter);   // 'A'

int bigCode = 70000;          // exceeds char range (0-65535)
char wrapped = (char) bigCode;
System.out.println((int) wrapped);  // 446470000 % 65536

// Negative int to char — wraps since char is unsigned:
int negative = -1;
char fromNeg = (char) negative;
System.out.println((int) fromNeg);  // 65535 — -1 interpreted as unsigned max

int neg2 = -65;
char fromNeg2 = (char) neg2;
System.out.println((int) fromNeg2);  // 65471

// char to byte and short — requires explicit cast:
char c = 'A';                // 65
byte b = (byte) c;           // 65 — fits fine
System.out.println(b);       // 65

char highChar = '\u0100';   // 256 — outside byte range
byte narrowed = (byte) highChar;
System.out.println(narrowed);  // 0256 wraps to 0 in byte

// char to shortchar is unsigned, short is signed:
char big = '\uFF00';        // 65280 — valid char, but exceeds short max
short s = (short) big;
System.out.println(s);       // -256 — sign bit interpreted differently

Narrowing Reference Types — Downcasting

For object references, narrowing means downcasting — converting a parent type reference to a child type. This requires an explicit cast and can throw ClassCastException at runtime if the object isn't actually an instance of the target type.
Java
class Animal { void speak() { System.out.println("..."); } }
class Dog extends Animal {
    void speak() { System.out.println("Woof!"); }
    void fetch() { System.out.println("Fetching!"); }
}
class Cat extends Animal {
    void speak() { System.out.println("Meow!"); }
}

// Upcasting first (widening — automatic):
Animal a = new Dog();   // a holds a Dog, typed as Animal

// Downcasting (narrowing — explicit cast required):
Dog d = (Dog) a;        // safe — a actually IS a Dog
d.fetch();              // works fine

// Wrong downcast — compiles but throws at runtime:
Animal a2 = new Cat();
Dog d2 = (Dog) a2;      // compiles fine
                        // throws ClassCastException at runtime:
                        // class Cat cannot be cast to class Dog

// Always guard with instanceof before downcasting:
Animal unknown = getAnimal();

// Traditional approach:
if (unknown instanceof Dog) {
    Dog safeDog = (Dog) unknown;
    safeDog.fetch();
}

// Pattern matching (Java 16+) — cleaner, no separate cast needed:
if (unknown instanceof Dog safeDog) {
    safeDog.fetch();   // safeDog is already typed as Dog
}

// Switch pattern matching (Java 21+):
switch (unknown) {
    case Dog dog   -> dog.fetch();
    case Cat cat   -> cat.speak();
    case Animal a3 -> a3.speak();
}

Safe Narrowing Patterns

When you must narrow, use these patterns to avoid silent data corruption and unexpected behavior:
Java
// ── Pattern 1: Range check before narrowing int/long ────────────
long value = getLongValue();
if (value < Byte.MIN_VALUE || value > Byte.MAX_VALUE) {
    throw new ArithmeticException("Value out of byte range: " + value);
}
byte safe = (byte) value;

// ── Pattern 2: Math.toIntExact() for longint ──────────────────
// Throws ArithmeticException on overflow instead of silently wrapping:
long longVal = 50_000L;
int intVal = Math.toIntExact(longVal);   // safe if within int range

// ── Pattern 3: Round before casting floating point ───────────────
double d = 9.7;
int truncated = (int) d;                  // 9  — is this what you want?
int rounded   = (int) Math.round(d);      // 10 — probably what you want
int floored   = (int) Math.floor(d);      // 9  — always rounds down
int ceiled    = (int) Math.ceil(d);       // 10 — always rounds up

// ── Pattern 4: instanceof before downcasting objects ────────────
if (animal instanceof Dog dog) {
    dog.fetch();   // Java 16+ pattern matching — safest approach
}

// ── Pattern 5: BigDecimal for precise decimal narrowing ──────────
import java.math.BigDecimal;
import java.math.RoundingMode;

BigDecimal bd = new BigDecimal("9.876");
int asInt = bd.setScale(0, RoundingMode.HALF_UP).intValue();  // 10
double asDouble = bd.doubleValue();  // 9.876

// ── Pattern 6: Clamp to target range before casting ──────────────
int original = 300;
byte clamped = (byte) Math.max(Byte.MIN_VALUE,
                               Math.min(Byte.MAX_VALUE, original));
// clamped = 127 — saturates at max instead of wrapping

Widening vs Narrowing — Side by Side

A direct comparison to cement the distinction:
Java
// ── Widening ─────────────────────────────────────────────────────
int i = 100;
double d = i;        // automatic — no syntax needed
long l = i;          // automatic
float f = i;         // automatic

// Safe: every int value fits in double, long, float
// No data loss for intlong or intdouble
// Possible precision loss for large intfloat (see widening tag)

// ── Narrowing ────────────────────────────────────────────────────
double pi = 3.14159;
int fromPi = (int) pi;       // explicit cast required: (int)
// fromPi = 3 — decimal truncated silently

long big = 10_000_000_000L;
int fromBig = (int) big;     // explicit cast required: (int)
// fromBig = 1410065408 — silently overflowed

// Summary table:
//
// Direction          Syntax          Safe?     Data loss?
// ─────────────────────────────────────────────────────────
// small → large      automatic       always    never*
// large → small      (type) value    never     possible
//
// * except long/intfloat precision loss (see widening tag)