Topic 5.5
Upcasting, Downcasting and instanceof
In one line
Upcasting (child to parent type) is automatic and always safe. Downcasting (parent to child type) needs an explicit cast, is checked at run time, and throws ClassCastException if the object isn't really that type, so you test first with instanceof.
Think of it like this
A parcel labelled 'Electronics'. Any phone can be put in a box labelled Electronics: that's always fine (upcasting). But if you open an 'Electronics' box and announce 'this is a phone!', you'd better check first, because it might be a toaster. Shouting 'phone' at a toaster doesn't make it one; the warehouse alarm goes off (ClassCastException). instanceof is opening the lid to look before you announce.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Cast
- Writing
(Type) expressionto view a value as another type. For references, it changes only the view, not the object. - Upcasting
- Treating an object as one of its parent types, like
Animal a = dog;. Automatic and always safe. - Downcasting
- Treating a parent-typed reference as a more specific child type, like
(Dog) animal. Needs the cast syntax and is checked at run time. - ClassCastException
- The exception the JVM throws when a downcast is wrong: the object isn't an instance of the target type.
- instanceof
- An operator that answers 'is this object a T (or a subtype of T)?' with true or false. Always false for null.
- Pattern variable
- The new variable in
x instanceof Dog d. It is declared and assigned only when the test succeeds. - checkcast
- The bytecode instruction a downcast compiles to. It verifies the object's type at run time.
Step by step
01Up is free
Assigning a Dog to an Animal variable compiles to nothing at all: no instruction, no check. The reference value (the address of the object) is copied as-is. The compiler already knows it's safe from the class hierarchy.
This happens all the time without you noticing: passing a Dog to a method that takes an Animal, adding a Dog to a List<Animal>, returning a Dog from a method declared to return Animal.
02Down needs proof
Going back down is different. An Animal variable might hold a Dog, a Cat, or a plain Animal. When you write (Dog) a, javac emits checkcast Dog. The JVM looks at the object's class pointer and checks whether that class is Dog or a subclass of Dog.
Yes: the same reference continues, now typed as Dog. No: ClassCastException, with a message naming both classes.
03What the exception says
The message names the object's real class, the type you asked for, and where each class came from (module and class loader). That last part matters in big applications where two different class loaders can each load a class called Dog; they are then different types, and a cast between them fails even though the names match.
04Test first with instanceof
a instanceof Dog asks the same question as the cast but answers with a boolean instead of an exception. The classic, pre-Java-16 shape is: test, then cast, then use.
null instanceof Anything is false, so the test also protects you from NullPointerException on the next line.
if (a instanceof Dog) {
Dog d = (Dog) a; // safe: we just checked
d.fetch();
}05Pattern matching for instanceof (Java 16)
The test-then-cast shape repeats the type twice, and nothing stops you from testing Dog and casting to Cat by mistake. Java 16 made the pattern form standard: if (a instanceof Dog d). When the test passes, d is declared, already cast.
The compiler tracks where the match is definitely true. With && you can use d on the right side: a instanceof Dog d && d.age() > 3. With || you can't (the match might not have happened). After if (!(a instanceof Dog d)) return; you can use d for the rest of the method.
if (a instanceof Dog d && d.isPuppy()) {
d.fetch();
}
if (!(obj instanceof String s)) {
return;
}
System.out.println(s.length()); // s is in scope here06Casts the compiler refuses
The compiler rejects a cast that can never succeed. A Dog variable can never hold a Cat, since they're siblings, so (Cat) dog is a compile error: incompatible types: Dog cannot be converted to Cat. Same for (Integer) someString.
Interfaces are subtler. (Runnable) someDog compiles if Dog is not final, because some subclass of Dog might implement Runnable. If Dog is final, no such subclass can exist, and the compiler rejects it. This is one reason final and sealed (Topics 5.10, 5.11) help the compiler help you.
Try it yourself
- 1
Rewrite play() with patterns
In the first example, rewrite
play()usingif (a instanceof Dog d) d.fetch(); else if (a instanceof Cat c) c.purr();. The output must not change. - 2
Cause and read a ClassCastException
In the second example, remove the try/catch around
(Integer) oand run. Read the full message: it namesjava.lang.String,java.lang.Integer, and the modulejava.baseboth come from. - 3
Find the compile-time refusals
Add
Cat c = (Cat) new Dog();to main and compile. Then tryRunnable r = (Runnable) new Dog();: it compiles (Dog isn't final). MarkDogasfinaland compile again to see the error appear.
Code & diagrams
Expected output
Playing with: dog
fetches the ball
Playing with: cat
purrs
Playing with: animal
same object: true
null instanceof Animal: false
cast of null: nullA Puppy passes a cast to Dog: a cast succeeds for the target type or any subclass of it.
Expected output
Dog -> Dog: OK, it is a Dog
Puppy -> Dog: OK, it is a Puppy
Cat -> Dog: Failed: ClassCastException
String is not an IntegerExpected output
big int 42
small int 7
string of length 2
something else: []
double 2.5
something else: [null]Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Cast between sibling classes
Cast a Dog variable directly to Cat.
class Animal { } class Dog extends Animal { } class Cat extends Animal { }
public class Main { public static void main(String[] a) { Dog d = new Dog(); Cat c = (Cat) d; } }Break #2
Downcast without checking
Store a Cat in an Animal variable and cast it to Dog with no instanceof test.
class Animal { }
class Dog extends Animal { }
class Cat extends Animal { }
public class Main { public static void main(String[] a) { Animal x = new Cat(); Dog d = (Dog) x; } }Myth vs fact
Myth
Casting converts an object into another type.
Fact
Reference casts never change the object. They change only the type the compiler uses for that expression, plus a run-time check for downcasts.
Myth
You should always check for null before instanceof.
Fact
instanceof already returns false for null. if (o != null && o instanceof Dog) has a redundant check.
Myth
Upcasting loses the subclass's data.
Fact
Nothing is lost. The object is unchanged; you just can't call Dog-only methods through an Animal-typed reference. Overridden methods still run Dog's code.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
checkcastandinstanceofare fast in HotSpot: each class stores a short 'primary supers' display of its superclass chain (up to depth 8) for a constant-time check, plus a one-element cache for secondary supers (interfaces and deep chains). A cache that keeps flipping between two interfaces at a hot site was a real JDK performance issue (JDK-8180450), addressed by a redesigned secondary-supers lookup in JDK 23. - ▸
Generic code is full of hidden casts:
String s = list.get(0);compiles toinvokeinterface List.getfollowed bycheckcast String, because generics are erased (Topic 8.6). That's where a 'heap pollution'ClassCastExceptionappears, far from the line that put the wrong object in. - ▸
In
equals,o instanceof Pointlets subclasses be equal to their parent, whilegetClass() == o.getClass()requires the exact same class.instanceofbreaks symmetry if a subclass adds fields to equality;getClass()breaks Liskov substitution. Records and final classes avoid the dilemma, which is part of why value classes are made final. - ▸
Since Java 21,
instanceofalso accepts record patterns, e.g.o instanceof Point(int x, int y)(Topic 11.3), and on Java 21 aninstanceofwhose pattern type is the same as the expression's type is allowed (it was an error in Java 16 and 17).
Remember this
- 1
Casting a reference never changes the object. It only changes the *declared type* through which you look at it.
(Dog) animaldoesn't convert anything; it tells the compiler 'trust me, treat this as a Dog' and asks the JVM to verify that claim. (Casting *primitives*, like(int) 3.9, is different: it really converts the value; see Topic 1.6.) - 2
Upcasting goes from a subtype to a supertype:
Animal a = new Dog();. It's implicit (no cast syntax needed) and can never fail, because every Dog is an Animal. You lose access to Dog-only methods through that reference, but the object keeps them. - 3
Downcasting goes from a supertype to a subtype:
Dog d = (Dog) a;. It needs explicit cast syntax and compiles to acheckcastbytecode instruction. At run time, if the object isn't a Dog (or a subclass of Dog), the JVM throwsClassCastException. Anullreference passes any cast and staysnull. - 4
x instanceof Treturnstrueifxis not null and its object could be cast toTwithout an exception. It isfalsefornull, so it doubles as a null check. The compiler rejectsinstanceoftests and casts that could never succeed, such asStringtoInteger, or between two sibling classes. - 5
Pattern matching for instanceof (Java 16) combines the test and the cast:
if (a instanceof Dog d) { d.fetch(); }. The variabledexists only where the test is known to be true, and it works with&&and with negation (if (!(o instanceof String s)) return; s.length();). It removes the repeated type name and the risk of casting to the wrong type. - 6
Frequent downcasting is a design smell. If code keeps asking 'are you a Dog? are you a Cat?', the behaviour probably belongs in an overridden method (Topic 5.4), or the type should be sealed and handled with an exhaustive switch (Topic 5.11).
Explain it without notes
What's the difference between upcasting and downcasting? Which one can fail, and when?
What exactly does (Dog) a do at run time?
When does the compiler reject a cast outright?
What does pattern matching for instanceof add over test-then-cast?
Practice
Write static double totalArea(Object[] items) that sums the area of every Circle and Square in the array (each with its own fields) and ignores anything else, using pattern matching.
Given Animal[] zoo = { new Dog(), new Cat(), new Dog() }, count how many are Dogs using instanceof.
Write a method that takes an Object and returns its length if it's a String, its size if it's a java.util.List, and -1 otherwise.
Trade-offs
- ↔
instanceof chains are quick to write but every new subtype means finding and updating every chain. Overridden methods (or sealed types with exhaustive switches) let the compiler find the places for you.
- ↔
Downcasting is sometimes unavoidable at boundaries (deserialisation,
equals, legacy APIs returningObject). Keep the casts at the edge and work with precise types inside.
Done when you can
Done when you can explain why upcasting is free and downcasting is checked.
Done when you can predict whether a cast fails to compile, fails at run time, or succeeds.
Done when you can use
instanceofpatterns with&&and negation.Done when you know
null instanceof Xis false and(X) nullis allowed.Done when you can recognise downcast-heavy code as a design smell and suggest overriding instead.