Upcasting & downcasting in Java
Implicit upcasts, explicit downcasts and ClassCastException.
(Dog) animal. Then, at runtime: ClassCastException. Why did the compiler let you walk into that?Upcasting: always safe
Treating a Dog as an Animal is an upcast. Every Dog is an Animal, so it's implicit — no cast needed, nothing can go wrong. You just see fewer methods.
Dog d = new Dog();
Animal a = d; // implicit upcastDowncasting: a promise you must keep
Going back — Animal to Dog — is a downcast. It needs an explicit cast (Dog) a, and the JVM checks it at runtime. If the object really is a Dog, you unlock Dog's own methods.
Animal a = new Dog();
Dog d = (Dog) a; // checked at runtime
d.fetch(); // Dog methods unlockedA real Dog
What does this print?
class Animal { }
class Dog extends Animal {
String fetch() { return "stick"; }
}
void main() {
Animal a = new Dog();
Dog d = (Dog) a;
System.out.println(d.fetch());
}stickCompile errorThrows ClassCastException
Show the answer
stick. The object really is a Dog, so the runtime check passes and fetch() becomes available.
A Cat in disguise
Now the Animal is a Cat. What happens?
class Animal { }
class Dog extends Animal { }
class Cat extends Animal { }
void main() {
Animal a = new Cat();
Dog d = (Dog) a;
System.out.println("ok");
}okCompile errorThrows ClassCastException
Show the answer
It throws **ClassCastException**. The compiler allowed the cast because an Animal *might* be a Dog. At runtime the JVM sees a Cat and refuses.
Unrelated classes
Dog and Car share no superclass chain, so no object could ever be both. The compiler knows the cast can't succeed and rejects it outright — compile error, not an exception.
class Dog { }
class Car { }
Dog d = new Dog();
Car c = (Car) d; // compile errorWhat does a cast do to the object?
After Dog d = (Dog) animal;, has the object been converted into a Dog?
Think about it, then reveal the answer
Nothing happens to the object. Objects never change class. A reference cast only changes the static type the compiler uses for that reference — after the JVM verifies the object already is one.
In real projects
Old pre-generics code was full of casts like (String) list.get(0), and a wrong one crashed at runtime. Today you'll mostly cast inside equals(Object) — and modern code prefers pattern matching (next lesson) to check and cast in one safe step.
Key takeaways
- Upcast: implicit, always safe
- Downcast: explicit (Dog) a, checked at runtime
- Wrong downcast throws ClassCastException
- Casts between unrelated classes don't compile
Generics are erased at runtime, so the compiler secretly inserts casts for you: String s = list.get(0); compiles to a checkcast instruction in the bytecode.
Practice questions
What does this print?
class Animal { }
class Dog extends Animal { }
class Cat extends Animal { }
void main() {
Animal a = new Cat();
Dog d = (Dog) a;
System.out.println("ok");
}- ok
- Compile error
- Throws ClassCastException
Check your answer
Throws ClassCastException. The compiler accepts the cast because an Animal might be a Dog. At runtime the object is a Cat, so the cast fails.
What does this print?
class Animal { }
class Dog extends Animal {
String fetch() { return "ball"; }
}
void main() {
Animal a = new Dog();
Dog d = (Dog) a;
System.out.println(d.fetch());
}- ball
- Compile error
- Throws ClassCastException
Check your answer
ball. The object really is a Dog, so the downcast succeeds and unlocks Dog's own methods.