☕ 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):
// double → float → long → int → short → byte
// ↓
// char
// Syntax: (targetType) expression
double d = 9.99;
float f = (float) d; // double → float
long l = (long) d; // double → long
int i = (int) d; // double → int
short s = (short) d; // double → short
byte b = (byte) d; // double → byte
// 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 = 3Decimal 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 truncatedOverflow — 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 byte — byte 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); // 0 — 256 wraps to 0
System.out.println((byte) e); // 44 — 300 - 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 short — short 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 ArithmeticExceptionNarrowing 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); // 4464 — 70000 % 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); // 0 — 256 wraps to 0 in byte
// char to short — char 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 differentlyNarrowing 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 long → int ──────────────────
// 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 wrappingWidening 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 int → long or int → double
// Possible precision loss for large int → float (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/int → float precision loss (see widening tag)Related Topics in Java Basics
Variables in Java
A variable is just a named box in memory that holds a value. Java is strict about what goes in each box — you tell it the type upfront. Once you get this, the rest of Java clicks into place.
Data Types in Java
Java needs to know exactly what kind of data it's dealing with before it can store or process it. Integers, decimals, characters, true/false — each has its own type. Knowing which to use (and why) makes your programs efficient and bug-free.
Primitive Data Types
Java has eight primitive data types — the most basic building blocks for storing data. Unlike objects, primitives are stored directly in memory, making them fast and efficient. Understanding each type, its size, range, and when to use it is fundamental to writing correct Java programs.
Non-Primitive Data Types
Non-primitive data types — also called reference types — are everything beyond Java's eight primitives. Strings, arrays, classes, interfaces, enums, and records all fall into this category. They're more powerful than primitives, but they work differently in memory, comparison, and nullability. Understanding the distinction is essential for writing correct Java.