Upcasting, downcasting, and instanceof
On This Lesson 11 topics
Lesson Overview
Last lesson you pointed a parent reference at a child object and called an overridden method. That raised a natural question. Once an object is held by a parent reference, what can you still do with it? And if you need something only the child has, how do you get back to the child type?
This lesson answers both. You will learn upcasting (child to parent, automatic and safe), downcasting (parent to child, explicit and risky), the instanceof check that makes downcasting safe, and the run-time error you get when you skip it. You will also learn why needing many downcasts usually means the design should change, which is the subject of the lesson after next.
Why this matters: in backend code, objects often arrive typed as something general (a parent class, or later an interface). Knowing exactly what the compiler lets you do with a general type, and what a wrong cast costs at run time, helps you read stack traces and write code that does not crash in production.
This lesson uses the account classes from the previous module and the previous lesson: Account, SavingsAccount with an extra getRate() method, and CheckingAccount.
Learning Objectives
By the end of this lesson you will be able to:
- Define upcasting and downcasting and say which one needs an explicit cast.
- Explain why a parent reference can call only methods the parent declares, and read the compiler error when you try otherwise.
- Predict when a downcast succeeds and when it throws
ClassCastException. - Use
instanceofwith a pattern variable to check and cast in one step. - State what
instanceofreturns fornull. - Recognize repeated downcasting as a design smell.
Written Explanation
Upcasting: always safe
Converting a child reference to a parent type is an upcast:
SavingsAccount s = new SavingsAccount("Ana", 1000.0, 0.02);
Account a = s; // upcast: no cast operator neededIt is automatic and cannot fail, because every SavingsAccount really is an Account. The Java Language Specification calls this a widening reference conversion: it never needs an action at run time and never throws an exception, because the compiler can prove it is correct.
Note what an upcast does not do. It does not copy or change the object. a and s refer to the very same object (the check a == s is true). Only the view of it changes.
What a parent reference lets you do
The compiler checks every call against the reference type. Through an Account reference you may call only members that Account declares (or inherits):
Account a = new SavingsAccount("Ana", 1000.0, 0.02);
System.out.println(a.getRate());Bad1.java:4: error: cannot find symbol
System.out.println(a.getRate());
^
symbol: method getRate()
location: variable a of type Account
1 errorThe object does have getRate(), but the compiler only sees the variable's declared type, and Account has no such method. This is a compile-time error, so you find out before the program runs. Overridden methods, such as monthEnd(), remain fully usable through the parent reference, and they still run the child's version.
Downcasting: explicit and risky
To get a child-typed view back, write the child type in parentheses. This is a downcast:
Account a = new SavingsAccount("Ana", 1000.0, 0.02);
SavingsAccount s = (SavingsAccount) a; // downcast
System.out.println(s.getRate()); // now allowedThe compiler allows the cast because a SavingsAccount could be hiding behind an Account reference. It cannot know whether one actually is. So Java checks at run time. If the object's real class is the target type or a subclass of it, the cast succeeds. If not, the program throws a ClassCastException. (A null reference passes the cast and stays null.) The specification describes the same rule: narrowing reference conversions get a run-time check.
Account a = new CheckingAccount("Omar", 500.0);
SavingsAccount s = (SavingsAccount) a; // compiles, then failsException in thread "main" java.lang.ClassCastException: class CheckingAccount cannot be cast to class SavingsAccount (CheckingAccount and SavingsAccount are in unnamed module of loader 'app')
at Ex2.main(Ex2.java:4)Read it like a sentence: the real object is a CheckingAccount, and you asked to treat it as a SavingsAccount. The line number in the stack trace (a skill from the debugging module) points at the cast. The program stops there, so a wrong cast is a bug found late. If the two types are completely unrelated (neither is a subclass of the other), the compiler rejects the cast outright instead.
instanceof: check before you cast
The instanceof operator asks "is the object behind this reference of this type (or a subclass)?" and gives a boolean:
if (a instanceof SavingsAccount) {
SavingsAccount s = (SavingsAccount) a;
System.out.println(s.getRate());
}Modern Java lets you combine the check, the cast, and the new variable into one step with a pattern variable:
if (a instanceof SavingsAccount s) {
System.out.println(s.getRate()); // s is already a SavingsAccount here
}The variable s exists only where the check is known to have succeeded. You can use it in the same condition after &&:
if (a instanceof SavingsAccount s && s.getRate() > 0.025) { ... }This works because && stops at the first false, so s.getRate() is only evaluated when the check passed. (Using || instead would not compile, because s might not exist.)
Two more facts:
instanceofisfalsefornull, so a null check is not needed before it. Example 3 below confirms this by running.x instanceof Accountistruefor aSavingsAccountobject: the check accepts the class and its subclasses.
If you are using an older Java version that does not support patterns, the two-line form above (check, then cast) does the same job.
The design smell
Downcasting is a legitimate tool, but a loop full of instanceof checks, each followed by a cast and a type-specific call, means the parent type is missing an operation. Last lesson showed the better fix: declare the operation on the parent (monthEnd()) and let each subclass override it. Rule of thumb: if the cast is only there to call the "same kind of thing" on each type, add a polymorphic method instead. Casting is justified when the information is truly specific to one subtype and does not make sense for the others.
Examples
Example 1: up then back down (simple)
SavingsAccount s = new SavingsAccount("Ana", 1000.0, 0.02);
Account a = s; // upcast
System.out.println(a.getOwner());
SavingsAccount back = (SavingsAccount) a; // downcast
System.out.println(back.getRate());
System.out.println(a == back);Output:
Ana
0.02
trueThe cast succeeded because the real object was a SavingsAccount all along. a == back is true: there is still only one object, seen through two reference types.
Example 2: a wrong downcast (practical)
Here the real object is a CheckingAccount, and the code tries two different casts. Only one of them is a bug.
Account a = new CheckingAccount("Omar", 500.0);
Account same = (Account) a; // fine: the object is an Account
SavingsAccount s = (SavingsAccount) a; // ClassCastExceptionThe first cast does nothing harmful (an upcast written out explicitly, redundant but legal). The second throws the exception shown earlier. The fix is not to catch the exception; it is to check first:
if (a instanceof SavingsAccount s) {
System.out.println(s.getRate());
} else {
System.out.println(a.getOwner() + " has no interest rate");
}Example 3: a safe, specific query over a mixed array (realistic)
Suppose a report needs the savings rates above 2.5%. A rate exists only on SavingsAccount, and it makes no sense to add one to checking accounts, so a type check is reasonable here. The array even contains a null.
Account[] accounts = {
new SavingsAccount("Ana", 1000.0, 0.02),
new CheckingAccount("Omar", 500.0),
null,
new SavingsAccount("Lina", 200.0, 0.03)
};
for (Account a : accounts) {
if (a instanceof SavingsAccount s && s.getRate() > 0.025) {
System.out.println(s.getOwner() + " has a high rate: " + s.getRate());
} else {
System.out.println("skipped " + (a == null ? "null" : a.getOwner()));
}
}Output:
skipped Ana
skipped Omar
skipped null
Lina has a high rate: 0.03Ana is skipped because her rate is only 0.02. Omar fails the type check. The null entry fails it too, and no NullPointerException occurs. Only Lina passes both tests.
Backend Insight
In real services, objects often come out of generic places: a framework hands you a general type, or a collection holds a parent type. A careless downcast there turns into a ClassCastException at 3 a.m., and the stack trace points to the cast, not to the real mistake, which is usually that the wrong type reached that code.
Two habits protect you. First, keep service code on the parent type and let subclasses carry the differences, so casts are rare. Second, when a cast is genuinely needed, guard it with instanceof and decide what the else branch should do (skip, log, or reject) rather than letting the program crash.
Common Mistakes
Mistake 1: Casting to "make the error go away"
- Incorrect: the compiler says
cannot find symbol, so the developer adds((SavingsAccount) a).getRate()everywhere. - Correct: ask whether the method belongs on the parent. If the operation makes sense for every account, declare it on
Account. If it is specific, check first withinstanceof.
Mistake 2: Assuming a cast changes the object
A cast changes only how the compiler sees the reference. It does not convert the object, and it does not change which overridden method runs. A SavingsAccount cast to Account still runs SavingsAccount.monthEnd().
Mistake 3: Casting without checking
(SavingsAccount) a compiles whenever SavingsAccount is a subtype of a's declared type, so the compiler gives you no warning. The failure appears only at run time, and only for some inputs, which makes it easy to miss in testing.
Mistake 4: Using instanceof chains for everything
Several instanceof checks in a row, each doing the same job for a different type, is the pattern lesson 4 replaces with polymorphism.
Exercises
Try each exercise yourself before opening the solutions.
Level A: Foundation
A1. With Animal a = new Dog(); where Dog extends Animal and Cat extends Animal, say for each line whether it compiles, and whether it runs without error: Dog d = a;, Dog d = (Dog) a;, Cat c = (Cat) a;.
Show solution A1
A1. Dog d = a; does not compile (parent to child needs an explicit cast). Dog d = (Dog) a; compiles and runs, because the real object is a Dog. Cat c = (Cat) a; compiles but throws ClassCastException at run time.
A2. Which direction of conversion (up or down) needs an explicit cast, and why?
Show solution A2
A2. Downcasting (parent to child). Upcasting is always valid, so Java does it automatically. A downcast might be wrong, so the compiler makes you say it out loud, and the JVM checks it when the line runs.
A3. What does x instanceof Animal return when x is null? What does it return when x refers to a Dog?
Show solution A3
A3. It returns false for null and true for a Dog.
Level B: Application
B1. An Animal[] zoo = { new Dog(), new Cat(), new Dog() } is given. Write a loop that counts the dogs using instanceof. What is the result?
Show solution B1
B1.
int dogs = 0;
for (Animal a : zoo) {
if (a instanceof Dog) dogs++;
}
System.out.println(dogs); // 2I compiled and ran it: the output is 2.
B2. Rewrite this unsafe code so it never throws ClassCastException:
Account a = findAccount();
SavingsAccount s = (SavingsAccount) a;
System.out.println(s.getRate());Show solution B2
B2.
Account a = findAccount();
if (a instanceof SavingsAccount s) {
System.out.println(s.getRate());
} else {
System.out.println("not a savings account");
}This also handles null, because instanceof is false for null.
B3. Explain why Account a = new SavingsAccount(...); a.getRate(); does not compile although the object has getRate(). Give two different ways to fix it and say when each is appropriate.
Show solution B3
B3. The compiler checks calls against the reference type Account, and Account declares no getRate(). Fix 1: use instanceof and a cast (or pattern variable), appropriate when the rate truly belongs only to savings accounts. Fix 2: if every account should be able to report something like a rate, declare the method on Account and override it where needed, which removes the need for a cast.
Level C: Challenge
C1. Write static double highestRate(Account[] accounts) that returns the highest rate among the savings accounts in a mixed array, and 0 if there are none.
Show solution C1
C1.
static double highestRate(Account[] accounts) {
double best = 0;
for (Account a : accounts) {
if (a instanceof SavingsAccount s && s.getRate() > best) {
best = s.getRate();
}
}
return best;
}I compiled and ran it: for rates 0.02 and 0.03 it returns 0.03, and for an array with only checking accounts it returns 0.0.
C2. A teammate's code has a loop with three instanceof checks, each followed by a cast and a call to a different method (addInterest(), chargeFee(), applyBonus()). What smell is this, and what is the first thing you would suggest?
Show solution C2
C2. It is the "type-check chain" smell: the parent type is missing an operation. Suggest declaring one method on Account (for example monthEnd()) and moving each branch into the matching subclass override, so the loop becomes a single call.
Mini Challenge
Using the account classes, write a small program with an Account[] of at least five objects including one null. Part 1: loop once and count how many entries are savings accounts, using instanceof. Part 2: write a method describe(Account a) that returns "savings, rate X" for savings accounts, "other account" for other non-null accounts, and "no account" for null. Part 3: deliberately write one unguarded cast that throws ClassCastException, run it, and read the first line of the message.
Check your work:
- You never cast without a preceding
instanceof(except in Part 3). - Your program does not crash on
null. - You can explain from the exception message which type the object really was and which type you asked for.
Summary & Next Lesson
Key takeaways
- Upcasting (child to parent) is automatic, safe, and does not change the object, only the view of it.
- Through a parent reference you can call only what the parent declares; overridden methods still run the child's version.
- Downcasting (parent to child) needs an explicit cast and is checked at run time: a wrong cast throws
ClassCastException. instanceofchecks the real type first; the pattern forma instanceof SavingsAccount schecks and gives you a typed variable. It isfalsefornull.- Many downcasts in a row mean the parent is missing a method. Use polymorphism instead.
Next lesson
You now know what the compiler allows and what the JVM checks. The next lesson, "Dynamic dispatch under the hood (conceptual)", explains how Java decides which method to run, and what is decided at compile time versus run time.
Self-check
- Which conversion is automatic: upcasting or downcasting?
- What exception does a wrong downcast throw, and when?
- Why can't you call
getRate()through anAccountreference? - What does
null instanceof SavingsAccountevaluate to? - When is a downcast a sign of a design problem?
Show answers
- Upcasting.
ClassCastException, at run time when the cast line executes and the real object is not of the target type.- The compiler checks against the reference type, and
Accountdoes not declaregetRate(). false.- When several casts exist only to call the same kind of action on different subtypes; a method on the parent, overridden in each subclass, would remove them.
References
- Oracle, Java Language Specification SE 21, 5.1.5 Widening Reference Conversion and 5.5 Casting Contexts. Supports the statements that upcasting needs no run-time action and cannot fail, and that narrowing reference conversions in casts get a run-time check that can throw
ClassCastException(null passes). - Oracle, Java Language Specification SE 21, 15.20.2 The instanceof Operator. I could not read the text of this section in the fetched page. The claims that
instanceofisfalsefornulland the scope rules of pattern variables are therefore confirmed by compiling and running the examples with JDK 21, not by quoting the specification. - Oracle, Java SE 21 API, Override annotation. Background for the
@Overrideusage in the example classes.
Practice
Apply this lesson with the module's lab, exercises and quiz.
Marking a lesson complete is optional and you can undo it any time. Your progress stays in this browser.