Type Casting
BeginnerUnderstand implicit widening, explicit narrowing casts for primitives, and safe object upcasting and downcasting with instanceof guards.
Overview
Type casting in Java comes in two flavours: primitive casting (between numeric types) and reference casting (between objects in a hierarchy). Widening (small → large type) is implicit and safe. Narrowing (large → small) requires an explicit cast and can lose data. For objects, upcasting (subtype → supertype) is always implicit; downcasting (supertype → subtype) requires an explicit cast and a runtime instanceof check to avoid ClassCastException.
Primitive Widening & Narrowing
Widening conversions are automatic: byte → short → int → long → float → double. No data loss for integral types widening to larger integral types, but long → float or long → double can lose precision because floating-point cannot represent all 64-bit integers exactly.
Narrowing conversions require an explicit cast: (int) 3.9 truncates to 3 — it does not round. Casting to a smaller type truncates high-order bits silently: (byte) 300 = 44 (300 % 256). This is a common source of overflow bugs.
public class PrimitiveCasting {
public static void main(String[] args) {
// Widening — implicit, safe
int i = 100;
long l = i; // int → long: implicit
float f = l; // long → float: implicit (may lose precision!)
double d = f; // float → double: implicit
System.out.println(d); // 100.0
// Precision loss: long → float
long big = 123_456_789_123L;
float approx = big; // implicit widening
System.out.println(big); // 123456789123
System.out.println(approx); // 1.23456794E11 — precision lost!
// Narrowing — explicit cast required
double pi = 3.14159;
int truncated = (int) pi; // truncates, does NOT round
System.out.println(truncated); // 3
// Truncation wraps on overflow
int big2 = 300;
byte small = (byte) big2; // 300 % 256 = 44
System.out.println(small); // 44
// Numeric promotion in expressions
byte a = 10, b = 20;
// byte result = a + b; // compile error! a+b is promoted to int
byte result = (byte)(a + b); // explicit cast back
System.out.println(result); // 30
// char ↔ int casting
char c = 'A';
int ascii = c; // widening char → int
System.out.println(ascii); // 65
char back = (char)(ascii + 1); // narrowing int → char
System.out.println(back); // B
}
}Reference Casting & ClassCastException
Upcasting assigns a subtype reference to a supertype variable — always safe, always implicit. The object is still the original subtype at runtime; you just see a narrower view of its API.
Downcasting recovers the original subtype reference. It fails at runtime with ClassCastException if the actual object is not the expected subtype. Always use instanceof before downcasting — or better, pattern-matching instanceof (Java 16+) which combines check and cast in one expression.
public class ReferenceCasting {
static class Animal { public String sound() { return "..."; } }
static class Dog extends Animal {
@Override public String sound() { return "Woof"; }
public String fetch() { return "Fetching!"; }
}
static class Cat extends Animal {
@Override public String sound() { return "Meow"; }
public String purr() { return "Purring..."; }
}
public static void main(String[] args) {
// Upcasting — implicit, safe
Animal a1 = new Dog(); // Dog IS-A Animal
Animal a2 = new Cat();
System.out.println(a1.sound()); // Woof — runtime dispatch
// a1.fetch(); // compile error — Animal doesn't have fetch()
// Old-style downcasting — manual check + cast
if (a1 instanceof Dog) {
Dog dog = (Dog) a1; // safe — instanceof confirmed
System.out.println(dog.fetch()); // Fetching!
}
// Pattern-matching instanceof (Java 16+) — check + bind in one line
if (a2 instanceof Cat cat) {
System.out.println(cat.purr()); // Purring...
}
// ClassCastException without guard
try {
Dog badDog = (Dog) a2; // a2 is actually a Cat
} catch (ClassCastException e) {
System.out.println("ClassCastException: " + e.getMessage());
}
// Array of mixed animals — polymorphic processing
Animal[] animals = { new Dog(), new Cat(), new Dog() };
for (Animal a : animals) {
String extra = switch (a) { // pattern switch (Java 21)
case Dog dog -> dog.fetch();
case Cat cat -> cat.purr();
default -> "unknown";
};
System.out.println(a.sound() + " | " + extra);
}
}
}Casting Pitfalls & Rules Summary
Key rules to remember:
1. Primitive widening: byte→short→int→long→float→double (left-to-right is automatic) 2. Narrowing truncates — it never rounds, and it wraps on overflow 3. long→float and long→double can lose precision even though it's a widening conversion 4. In arithmetic expressions, operands are promoted to at least int — even byte+byte is int 5. char is an unsigned 16-bit int — widening to int gives the Unicode code point 6. Object downcast without instanceof → ClassCastException at runtime 7. Arrays are covariant: String[] IS-A Object[], but assigning an Integer into a String[] throws ArrayStoreException
public class CastingPitfalls {
public static void main(String[] args) {
// Pitfall 1: long → float precision loss (widening but lossy)
long precise = 9_999_999_999_999_999L;
float lossy = precise; // widening — but float can't represent this!
System.out.println(precise); // 9999999999999999
System.out.println((long) lossy); // 10000000000000000 — wrong!
// Pitfall 2: byte arithmetic promotes to int
byte x = 127;
x = (byte)(x + 1); // must cast back to byte
System.out.println(x); // -128 — overflow wraps
// Pitfall 3: division stays integer if both operands are int
int a = 5, b = 2;
System.out.println(a / b); // 2 — not 2.5!
System.out.println((double) a / b); // 2.5 — cast ONE operand
// Pitfall 4: Array covariance → ArrayStoreException
Object[] objs = new String[3]; // upcasting array reference
objs[0] = "hello"; // fine
try {
objs[1] = 42; // runtime: ArrayStoreException
} catch (ArrayStoreException e) {
System.out.println("ArrayStoreException: " + e.getMessage());
}
// Pitfall 5: Comparing mixed numeric wrappers
Long lVal = 100L;
Integer iVal = 100;
System.out.println(lVal.equals(iVal)); // false — different types!
System.out.println(lVal.longValue() == iVal.intValue()); // true — compare primitives
}
}Key Points to Remember
- Widening (byte→int→long→double) is implicit; narrowing requires an explicit cast and can lose data
- Narrowing truncates toward zero — (int) 3.9 = 3; it never rounds
- Byte arithmetic is promoted to int — (byte)(a + b) needs the explicit cast back
- Upcasting is always safe; downcasting needs instanceof guard or throws ClassCastException
- Pattern-matching instanceof (Java 16+) is the modern way: if (a instanceof Dog dog)
- Array covariance (String[] IS-A Object[]) allows upcast but ArrayStoreException on wrong insert
Practice Type Casting in the Playground
Run and modify code directly in your browser - no setup needed.
Interview Questions
Sign in to ask AriaWhat is the difference between widening and narrowing type conversion?
What does (int) 3.99 evaluate to in Java?
What is ClassCastException and how do you prevent it?
Why does long → float sometimes lose precision even though it's a widening cast?
What is array covariance and what problem can it cause?
Ask Aria about Type Casting
Your personal AI tutor — ask anything about this concept