Definition
Two directions:
- Upcasting:
Account a = new SavingsAccount(...)– the subclass object is assigned to a parent reference. Always safe, no cast needed. You can only call Account methods througha, but overridden methods run the SavingsAccount version. - Downcasting:
SavingsAccount sa = (SavingsAccount) a– casting the parent reference back to the subclass type. Requires an explicit cast. ThrowsClassCastExceptionif the actual object is not a SavingsAccount.
Upcasting: treating a specific savings account passbook as a generic "account" – you can do general account operations but not savings-specific ones. Downcasting: checking that the generic account is actually a savings account, then using savings-specific features. If it is actually a current account, the check fails.
Understanding the Concepts
Upcasting (always safe)
Upcasting assigns a subclass object to a parent reference. It is implicit and always safe because a subclass IS-A parent type. After upcasting, you can only call methods defined on the reference type, but overridden methods still run the subclass version – this is polymorphism.
package com.bank.core;
import java.math.BigDecimal;
public class Account {
protected BigDecimal balance;
public Account(BigDecimal balance) {
this.balance = balance;
}
public BigDecimal getBalance() { return balance; }
public BigDecimal calculateInterest() {
return BigDecimal.ZERO; // base: no interest
}
}
public class SavingsAccount extends Account {
private double interestRate;
public SavingsAccount(BigDecimal balance, double rate) {
super(balance);
this.interestRate = rate;
}
@Override
public BigDecimal calculateInterest() {
return balance.multiply(BigDecimal.valueOf(interestRate / 100));
}
public double getInterestRate() { return interestRate; }
}
SavingsAccount sa = new SavingsAccount(new BigDecimal("500000"), 4.0);
Account a = sa; // upcasting: implicit, safe
a.getBalance(); // OK: Account method
// a.getInterestRate(); // COMPILE ERROR: Account does not have this method
a.calculateInterest(); // runs SavingsAccount.calculateInterest() → 20000
The reference type Account limits what you can call. The object type SavingsAccount determines which implementation runs.
Downcasting (needs check)
Downcasting casts a parent reference back to a subclass type. It requires an explicit cast and can throw ClassCastException if the actual object is not the expected type.
Account a = new SavingsAccount(new BigDecimal("500000"), 4.0);
// Safe downcast with pattern matching (Java 16+)
if (a instanceof SavingsAccount sa) {
System.out.println("Rate: " + sa.getInterestRate()); // SavingsAccount-specific method
}
// Traditional safe downcast
if (a instanceof SavingsAccount) {
SavingsAccount sa2 = (SavingsAccount) a;
System.out.println("Rate: " + sa2.getInterestRate());
}
// Unsafe downcast: ClassCastException at runtime
Account b = new CurrentAccount(new BigDecimal("250000"));
// SavingsAccount wrong = (SavingsAccount) b; // ClassCastException!
Reference type vs object type
This is the core concept behind polymorphism and one of the most important things to understand for interviews.
Account a = new SavingsAccount(new BigDecimal("500000"), 4.0);
// Reference type: Account → decides WHAT you can call (compile time)
// Object type: SavingsAccount → decides WHICH version runs (runtime)
a.getBalance(); // Account has this → allowed at compile time
// a.getInterestRate(); // Account does not have this → COMPILE ERROR
a.calculateInterest(); // SavingsAccount version runs (polymorphism)
How is this used in banking service design?
Upcasting enables polymorphic processing of different account types through a common interface.
package com.bank.service;
import java.math.BigDecimal;
import java.util.List;
public class InterestService {
// Process all account types uniformly through the parent reference
public BigDecimal calculateTotalInterest(List<Account> accounts) {
BigDecimal total = BigDecimal.ZERO;
for (Account account : accounts) {
// Each account type's calculateInterest() runs automatically
total = total.add(account.calculateInterest());
}
return total;
}
}
// Usage:
List<Account> accounts = List.of(
new SavingsAccount(new BigDecimal("500000"), 4.0),
new CurrentAccount(new BigDecimal("250000")),
new SavingsAccount(new BigDecimal("100000"), 3.5)
);
interestService.calculateTotalInterest(accounts); // polymorphic dispatch
The service does not need to know which specific account type it is processing. Each type's overridden calculateInterest() runs automatically.
What are common mistakes?
- Downcasting without instanceof. Throws ClassCastException at runtime. Always check first.
- Confusing compile-time type with runtime type. The reference type limits what you can call. The object type decides what runs.
- Using instanceof chains instead of polymorphism. If you find yourself writing
if (a instanceof SavingsAccount) ... else if (a instanceof CurrentAccount), consider adding the method to Account and overriding in each subclass.
What should a senior engineer know?
- Pattern matching (Java 16+):
if (a instanceof SavingsAccount sa)combines the check and cast in one clean statement, eliminating the risk of a cast error. - Sealed classes (Java 17+) with switch enable exhaustive type matching without instanceof chains:
switch (account) { case SavingsAccount sa -> ... case CurrentAccount ca -> ... }. - Visitor pattern is the design-pattern alternative to instanceof when you need different behavior per subtype without modifying the type hierarchy.
- At National Bank of India, Rajesh designs account processing around polymorphism so that adding a new account type (like FixedDepositAccount) requires only a new subclass with an overridden method, not changes to every service that processes accounts.
Visual Overview
Account a = new SavingsAccount(...);
Compile time (reference type: Account):
a.getBalance() → OK (Account has it)
a.getInterestRate() → ERROR (Account does not have it)
Runtime (object type: SavingsAccount):
a.calculateInterest() → SavingsAccount version (overridden)
Downcast:
if (a instanceof SavingsAccount sa) → safe
(SavingsAccount) a → ok if actually SavingsAccount
(SavingsAccount) currentAccount → ClassCastException!
Key Takeaway
Upcasting assigns a subclass object to a parent reference and is always safe and implicit. Downcasting casts a parent reference back to a subclass type and requires an explicit cast that can throw ClassCastException.
The reference type determines which methods you can call at compile time. The object type determines which overridden implementation runs at runtime. This is polymorphism.
Always check with instanceof before downcasting. Since Java 16, pattern matching combines the check and cast in one clean statement.
One sentence to remember: the reference type decides what you can call, the object type decides what runs – upcast is safe, downcast needs instanceof.
---