Java Type Casting: Widening, Narrowing, instanceof & ClassCastException
Type casting tells the Java compiler to treat a value as a different type. Java supports two kinds of casting: widening conversions, which are safe and automatic (int to long, Dog to Animal), and narrowing conversions, which may lose information and require an explicit cast operator (long to int, Animal to Dog). Incorrect narrowing of reference types throws ClassCastException at runtime. This article covers both numeric and reference casting, safe-cast patterns with instanceof, and the modern pattern-matching instanceof syntax.
Widening Conversions: Automatic and Safe
A widening conversion promotes a value to a broader type that can represent all the same values and more. Because no information is lost, the compiler performs widening automatically without requiring any syntax from the programmer. For primitives, widening follows the chain: byte → short → int → long → float → doublechar → intDogAnimalDogAnimal
Widening can introduce subtle numeric precision loss in one case: converting a large intlongfloatdouble
// Primitive widening — automatic
byte b = 42;
short s = b; // byte → short (automatic)
int i = s; // short → int (automatic)
long l = i; // int → long (automatic)
float f = l; // long → float (automatic — but may lose precision!)
double d = f; // float → double (automatic)
// Precision loss example
int big = 123_456_789;
float imprecise = big; // widening: int → float
System.out.println(big); // 123456789
System.out.println(imprecise); // 1.23456792E8 — last digits lost!
System.out.println((int) imprecise); // 123456792 — not the original value
// Reference widening — also automatic
class Animal { String name; }
class Dog extends Animal {}
Dog dog = new Dog();
Animal animal = dog; // widening: Dog → Animal (no cast needed)
Object obj = animal; // widening: Animal → Object (no cast needed)
Narrowing Numeric Conversions
A narrowing conversion converts a value to a type with a smaller range. For primitives this means going down the widening chain: double → float → long → int → short → byte(targetType)
The truncation rules for integer narrowing are deterministic but surprising. When casting a longintInteger.MAX_VALUE
// Integer narrowing — truncates bits
long big = 300L;
int narrow = (int) big; // OK: 300 fits in int
System.out.println(narrow); // 300
long overflow = 3_000_000_000L;
int truncated = (int) overflow; // 3_000_000_000 > Integer.MAX_VALUE
System.out.println(truncated); // -1294967296 (bit truncation, not an error!)
// Floating-point to integer — drops fractional part, clamps at range limits
double pi = 3.99;
int piInt = (int) pi; // truncates toward zero → 3 (not rounded!)
System.out.println(piInt); // 3
double huge = 1e18;
int hugeInt = (int) huge; // out of int range → Integer.MAX_VALUE (2147483647)
System.out.println(hugeInt);
double nan = Double.NaN;
int nanInt = (int) nan; // NaN → 0
System.out.println(nanInt); // 0
// byte/short narrowing — keeps low-order bits
int val = 200;
byte b = (byte) val; // 200 in binary = 11001000; as signed byte = -56
System.out.println(b); // -56
// char ↔ int narrowing and widening
char c = 'A';
int code = c; // widening: char → int (65)
char back = (char) 66; // narrowing: int → char ('B')
System.out.println(code + " " + back); // 65 B
Math.toIntExact(long)longintArithmeticExceptionReference Casting and ClassCastException
Narrowing a reference type, casting a supertype to a subtype, is called a downcast. The compiler allows it because it cannot determine at compile time whether the actual runtime object is of the target type. The JVM performs the type check at runtime and throws ClassCastExceptionClassCastExceptionRuntimeException
The safe way to downcast is to check the runtime type first with instanceofinstanceoftrue
class Animal { String sound() { return "..."; } }
class Dog extends Animal { String fetch() { return "fetching!"; } }
class Cat extends Animal { String purr() { return "purring!"; } }
Animal a1 = new Dog(); // widening: Dog stored as Animal
Animal a2 = new Cat(); // widening: Cat stored as Animal
// Unsafe downcast — ClassCastException if wrong type
Dog d = (Dog) a1; // OK: actual type IS Dog
System.out.println(d.fetch()); // fetching!
try {
Dog fail = (Dog) a2; // WRONG: actual type is Cat
} catch (ClassCastException e) {
System.out.println("Cannot cast Cat to Dog: " + e.getMessage());
}
// Safe downcast — check with instanceof first
if (a2 instanceof Cat) {
Cat c = (Cat) a2; // guaranteed to succeed
System.out.println(c.purr());
}
// Java 16+ — pattern matching instanceof (combines check and cast)
if (a1 instanceof Dog dog) {
System.out.println(dog.fetch()); // 'dog' is already typed as Dog
}
// Useful in switch for dispatch over a hierarchy
Object obj = 42;
String description = switch (obj) {
case Integer i -> "Integer: " + i;
case String s -> "String: " + s;
default -> "Other: " + obj;
};
System.out.println(description); // Integer: 42
Casting with Interfaces and Arrays
Reference casting also applies to interfaces. An object can be cast to any interface type it implements, regardless of where in the hierarchy the interface is declared. The JVM verifies at runtime that the actual object implements the target interface.
Arrays have their own casting rules. An array of a subtype is a subtype of an array of the supertype, Dog[]Animal[]ArrayStoreException
// Interface casting
interface Serializable {}
interface Printable { void print(); }
class Document implements Serializable, Printable {
@Override public void print() { System.out.println("Document"); }
}
Object obj = new Document();
Printable p = (Printable) obj; // valid: Document implements Printable
p.print();
Serializable ser = (Serializable) obj; // also valid
// Array covariance — safe widening
Dog[] dogs = { new Dog(), new Dog() };
Animal[] animals = dogs; // widening: Dog[] → Animal[] (automatic)
// Array store check — throws ArrayStoreException at runtime
try {
animals[0] = new Cat(); // ← putting a Cat into a Dog[] via Animal[] ref
} catch (ArrayStoreException e) {
System.out.println("Cannot store Cat in Dog[]: " + e.getMessage());
}
// instanceof with arrays
System.out.println(dogs instanceof Animal[]); // true — due to covariance
System.out.println(dogs instanceof Dog[]); // true
System.out.println(dogs instanceof Cat[]); // false
Dog[]Animal[]List<? extends Animal>