What happens when a child object is assigned to a parent reference?
Animal a = new Dog(); creates a Dog object and stores a reference to it in a variable of type Animal. Two types are now involved, and each decides something different:
- The reference type (Animal) decides what you can call. The compiler only allows methods and fields declared in Animal.
- The object type (Dog) decides which implementation runs for instance methods.
a.sound()runs Dog's override. This is dynamic dispatch, the heart of polymorphism. - Fields and static methods are not polymorphic. They're resolved by the reference type, so
a.namereads Animal's field even when Dog declares its own.
Going up (child to parent, upcasting) is automatic and always safe. Going down (downcasting) needs an explicit cast and is checked at run time: if the object isn't really that type, you get ClassCastException. Check first with instanceof, which since Java 16 also does the cast for you.
Why this matters: it lets you write code against a general type (List, PaymentGateway) and plug in any implementation later.
Example
class Animal {
String name = "animal";
void sound() { System.out.println("..."); }
static String kind() { return "Animal"; }
}
class Dog extends Animal {
String name = "dog"; // hides Animal.name (avoid this in real code)
@Override void sound() { System.out.println("Woof"); }
static String kind() { return "Dog"; } // hides, doesn't override
void fetch() { System.out.println("Fetching!"); }
}
Animal a = new Dog(); // upcast: implicit and always safe
a.sound(); // Woof object type decides (dynamic dispatch)
System.out.println(a.name); // animal fields: reference type decides
System.out.println(Animal.kind()); // Animal static: resolved by type, never by object
// a.fetch(); // compile error: Animal has no fetch()
if (a instanceof Dog d) { // Java 16: check and cast in one step
d.fetch(); // Fetching!
}
Animal plain = new Animal();
Dog oops = (Dog) plain; // compiles, but throws ClassCastException at run timeAnimal a = new Dog();
1. new Dog() -> the heap gets ONE object with Dog's class pointer and BOTH name fields
2. a -> a reference (address) typed as Animal; the object itself is unchanged
a.sound()
3. javac emits invokevirtual Animal.sound() (checked against the reference type)
4. the JVM looks up sound() in the object's class (Dog) -> runs Dog.sound()
(the JIT often inlines it when only one implementation is loaded)Common mistake
Calling an overridable method from a constructor. The override runs before the subclass has initialised its fields.
Under the hood
Casting never changes the object, only how you're allowed to see it. That's why downcasting can fail: the compiler trusts you, the JVM checks. A subtle consequence of dynamic dispatch: if a parent constructor calls an overridable method, the child's override runs before the child's fields are initialised, so it sees null and 0. Never call overridable methods from constructors.
Check yourself
Animal a = new Dog(); both classes declare a field called name. What does a.name read?
How this connects
Where this leads
You've reached the end of this thread. Try a learning path for what's next.
Part of OOP in practice.
Was this lesson helpful?
Finished reading? Mark it complete to track your progress.