Stage 2: Object-oriented programming, lesson 7 of 15

What happens when a child object is assigned to a parent reference?

Intermediate3 min read@since 16Code runs on your Java 25
Explain it forThe essentials plus production detail and pitfalls.

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.name reads 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.

Memory lab

Example

Java
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 time
What actually happens inside the JVM
Animal 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.