Java • Object-Oriented Programming

Polymorphism — Use polymorphism with superclass references (conceptual)

Discover how one variable type can trigger many different behaviors at runtime.

Java programming emblem with a coffee cup, code braces, and terminal
Inside this lesson 10 sections 5 questions 5 figures

SECTION 1

Historical Context & Motivation

Imagine you are building a drawing application. You need to store circles, rectangles, and triangles in a single list and tell each one to draw itself. Without polymorphism, you would have to write separate code paths for every shape type — and every time you invented a new shape, you would have to rewrite that logic. Polymorphism solves this problem by letting you treat many different object types through a single, shared superclass reference.

The word polymorphism comes from the Greek words poly (many) and morph (form). In programming, it means that a single method call can take many forms depending on the actual object behind the reference. This idea did not appear overnight — it evolved across decades of computer science research.

1967 • Simula Introduces Classes

Ole-Johan Dahl and Kristen Nygaard create Simula, the first language with classes and inheritance. It plants the seed for polymorphism by allowing subclasses to override methods.

1972 • Smalltalk & Message Passing

Alan Kay's Smalltalk language popularizes the idea that objects respond to messages. Different objects can respond to the same message in their own way — pure polymorphism.

1983 • C++ Adds Virtual Functions

Bjarne Stroustrup designs C++ with virtual functions, bringing compile-time and runtime polymorphism to a widely used systems language.

1995 • Java Makes Polymorphism Default

James Gosling releases Java. Every non-static method is virtual by default, making runtime polymorphism the standard rather than the exception.

So here is the central question this lesson explores: How can a variable declared as a superclass type call the correct overridden method in a subclass — and why is this so powerful?

SECTION 2

Core Principles & Definitions

Before diving into code, you need to understand a handful of foundational ideas. Each one builds on the concepts of classes, inheritance, and method overriding that you have already encountered. Together, they form the conceptual backbone of polymorphism through superclass references.

1

Superclass Reference

A variable whose declared type is a parent class (e.g., Animal a). It can point to any subclass object (Dog, Cat, etc.).
2

Actual (Runtime) Type

The actual type is decided by the constructor used with new. In Animal a = new Dog();, the runtime type is Dog.
3

Dynamic Method Dispatch

When you call a method on a superclass reference, Java chooses the overridden version in the actual subclass at runtime — not at compile time.
4

"Is-A" Relationship

Polymorphism relies on the fact that a subclass "is a" kind of its superclass. A Dog is an Animal, so an Animal reference can legally hold a Dog.
5

Compile-Time vs. Runtime View

The compiler sees only the declared type and checks that the method exists there. The JVM sees the actual type and runs the correct version.
✦

KEY TAKEAWAY

Think of a TV remote control. You press the "power" button (one interface), and it turns on whichever TV brand is plugged in — Samsung, LG, Sony. The remote doesn't need to know the brand; each TV implements "power on" its own way. A superclass reference is like that remote: it sends the command, and the actual object decides how to respond.

SECTION 3

Visual Explanation — The Polymorphism Pipeline

The diagram below shows how a single superclass reference (Animal a) can point to different subclass objects and how method calls are resolved at runtime through dynamic method dispatch. Follow the arrows from the variable declaration on the left, through the assignment, and down to the method that actually executes.

Polymorphism with Superclass ReferencesDeclared TypeAnimal a;assignmentRuntime Typenew Dog()a.speak()(compile-time check)JVM looks at actual object type ↓Dogspeak() → "Woof!"Catspeak() → "Meow!"Birdspeak() → "Tweet!"Result: One line of code — a.speak() — produces different outputdepending on whether 'a' currently references a Dog, Cat, or Bird object.
The top-left box represents the declared type (Animal). The top-right box shows the runtime type assigned via new. The three colored boxes at the bottom represent the subclass-specific implementations the JVM selects at runtime.

Notice that the calling code never mentions Dog, Cat, or Bird directly — it only knows about Animal. This is the beauty of polymorphism: the caller is decoupled from the specific subclass. If you add a Fish class later with its own speak() method, the calling code does not need to change at all.

SECTION 4

How It Works — The Compile-Time & Runtime Split

Understanding polymorphism requires separating what happens at two distinct moments: compile time (when the Java compiler checks your code) and runtime (when the Java Virtual Machine actually executes it). These two stages have different jobs and look at different information.

Compile Time — What the Compiler Sees

The compiler only knows the declared type of the variable. When you write Animal a = new Dog();, the compiler sees a as an Animal. If you call a.speak(), the compiler confirms that Animal has a speak() method. If you tried to call a.fetch() — a method defined only in Dog — the compiler would give an error, because Animal does not have fetch().

Runtime — What the JVM Executes

At runtime, the JVM looks at the actual object in memory. Since a really holds a Dog object, the JVM calls Dog's version of speak(). This process is called dynamic method dispatch (or late binding). The binding of the method call to the method body is deferred until the program is running.

⚠

Important Rule

The declared type determines which methods you can call (compile-time check). The actual type determines which version of the method runs (runtime dispatch).

The Three Conditions for Polymorphism

  1. Inheritance — A subclass must extend a superclass (Dog extends Animal).
  2. Method overriding — The subclass must provide its own implementation of a method declared in the superclass.
  3. Superclass reference — The variable holding the object must be declared as the superclass type.

If any one of these three conditions is missing, polymorphism does not occur. For example, if Dog does not override speak(), then the inherited Animal version runs — there is nothing polymorphic about that because only one version exists.

SECTION 5

Detailed Breakdown — Class Hierarchy & Method Resolution

Let's examine a complete class hierarchy to see exactly how polymorphism works with superclass references. The diagram below shows the Shape hierarchy — a superclass with three subclasses, each overriding the area() and describe() methods.

Shape Class Hierarchy — Polymorphic Method ResolutionShape (superclass)+ area(): double+ describe(): String // returns "Shape"extendsCircle- radius: double+ area(): double // π × r²Rectangle- width, height: double+ area(): double // width × heightTriangle- base, height: double+ area(): double // 0.5 × base × heightPolymorphic CodeShape s1 = new Circle(5); // s1.area() → 78.54Shape s2 = new Rectangle(4,6); // s2.area() → 24.0Shape s3 = new Triangle(3,8); // s3.area() → 12.0
The Shape superclass defines area() and describe(). Each subclass overrides area() with its own formula. The dashed box at the bottom shows how Shape references call the correct subclass method.
CodeDeclared TypeActual TypeMethod That Runs
Shape s = new Circle(5);ShapeCircleCircle's area() → π × 5² ≈ 78.54
Shape s = new Rectangle(4,6);ShapeRectangleRectangle's area() → 4 × 6 = 24.0
Shape s = new Triangle(3,8);ShapeTriangleTriangle's area() → 0.5 × 3 × 8 = 12.0

How the JVM resolves method calls for each superclass reference

SECTION 6

Worked Example — Tracing Polymorphic Method Calls

Let's trace through a complete example with the Animal hierarchy. Suppose we have these classes: Animal (superclass) with method speak() that returns "..." , Dog (subclass) overriding speak() to return "Woof!", and Cat (subclass) overriding speak() to return "Meow!". Dog also has a unique method fetch().

What Does This Code Print?

  1. 1

    Step 1 — Identify the Declarations

    We have two lines of code: Animal a1 = new Dog(); and Animal a2 = new Cat();. Both variables are declared as type Animal (superclass reference), but each holds a different subclass object.
    Declared type of a1 and a2: Animal
  2. 2

    Step 2 — Identify the Actual (Runtime) Types

    The new keyword tells us the actual types. a1 holds a Dog object; a2 holds a Cat object.
    Actual type of a1: Dog · Actual type of a2: Cat
  3. 3

    Step 3 — Trace a1.speak()

    Compile-time check: Does Animal have speak()? Yes — so the code compiles. Runtime dispatch: The JVM sees that a1 is actually a Dog. Dog overrides speak(), so the Dog version runs.
    Output: "Woof!"
  4. 4

    Step 4 — Trace a2.speak()

    Same compile-time check passes. At runtime, a2 is actually a Cat. Cat's overridden speak() runs.
    Output: "Meow!"
  5. 5

    Step 5 — What About a1.fetch()?

    Even though a1 is actually a Dog, the compiler only sees the declared type (Animal). Animal does not have a fetch() method, so this line causes a compile-time error. You would need to cast: ((Dog) a1).fetch();.
    Compile error — method not found in declared type
✦

KEY TAKEAWAY

Think of it like a school intercom. The principal (caller) says "Everyone, please announce your club." Every classroom (subclass) responds differently — the chess club says "Chess!", the drama club says "Theater!" The principal used one message, but each classroom responded in its own way. The principal doesn't need to know which club is in which room — that is polymorphism.

SECTION 7

Strengths, Limitations & Common Pitfalls

Polymorphism through superclass references is one of the most powerful tools in object-oriented programming, but it comes with trade-offs you should understand. The table below lays out the advantages and the common pitfalls students encounter.

StrengthsLimitations / Pitfalls
Flexibility — One variable type works for many subclasses. You can write methods that accept the superclass and work with any subclass.Loss of subclass-specific methods — A superclass reference cannot call methods unique to the subclass without casting.
Extensibility — Adding a new subclass requires zero changes to existing polymorphic code.Incorrect casting — Casting to the wrong subclass causes a ClassCastException at runtime.
Cleaner code — Eliminates long chains of if/else or switch statements to check object type.Forgetting to override — If a subclass does not override a method, the superclass version runs silently, which can be a subtle bug.
Supports arrays & collections — You can store mixed subclass objects in a single array of the superclass type.Confusion about fields — Polymorphism applies only to methods, not to instance variables. Field access is resolved by declared type.
⚠

Common Mistake

Students often think that polymorphism applies to instance variables the same way it applies to methods. It does not. If both a superclass and subclass define a field with the same name, the version used depends on the declared type, not the actual type. Only methods are dispatched dynamically.
🔑

WHY IT MATTERS

Polymorphism is the reason large Java applications (like Android apps or web servers) can grow without rewriting core logic. Each new feature is a new subclass that plugs into existing code through superclass references — the rest of the system never needs to know the details.

SECTION 8

Connection to Advanced Concepts

The superclass-reference style of polymorphism you have learned is often called runtime (or subtype) polymorphism. Java supports other forms of polymorphism as well, and understanding where superclass references fit in the bigger picture will prepare you for more advanced Java courses.

FeatureSuperclass Reference Polymorphism (This Lesson)Advanced Variations
MechanismMethod overriding + inheritanceInterfaces, abstract classes, generics, method overloading (compile-time polymorphism)
When resolvedRuntime (dynamic dispatch)Overloading resolved at compile time; interfaces use runtime dispatch like superclass references
Reference typeConcrete superclassInterface type, abstract class type, or generic type parameter
Use caseShared behavior among related classes in a hierarchyShared behavior among unrelated classes (interfaces) or type-safe collections (generics)

How superclass polymorphism compares to other Java polymorphism features

As you continue studying Java, you will encounter interfaces and abstract classes, which take the same core idea — calling a method on a general reference and having the actual object decide what to do — and make it even more flexible. Interfaces let completely unrelated classes share a common reference type, while abstract classes let you enforce that subclasses must override certain methods. Both rely on the same dynamic dispatch mechanism you learned here.

SECTION 9

Practice Problems

Question 1: CONCEPTUAL

In the statement Animal a = new Dog();, what is the declared type and what is the actual (runtime) type? Why does this distinction matter for polymorphism?
Show answer
The declared type is Animal and the actual (runtime) type is Dog. This distinction matters because the compiler uses the declared type to decide which methods you are allowed to call, while the JVM uses the actual type to decide which version of an overridden method runs. Without this two-phase process, the compiler would not be able to verify your code at compile time, and the JVM would not be able to run the subclass-specific version at runtime. This is the foundation of dynamic method dispatch.

Question 2: BASIC CALCULATION

Given the Shape hierarchy from this lesson, what value is returned by s.area() if Shape s = new Circle(10);? Show your reasoning.
Show answer
The variable s is declared as Shape, but the actual object is a Circle with radius 10. Because Circle overrides area(), the JVM runs Circle's version: π × 10² = π × 100 ≈ 314.16. Even though the reference is Shape, the Circle implementation executes due to dynamic method dispatch.

Question 3: INTERMEDIATE

Consider this code: Animal[] zoo = { new Dog(), new Cat(), new Dog() }; A loop calls zoo[i].speak() for each element. What output is produced, and why doesn't the code need to check which type each element is?
Show answer
The output is: "Woof!", "Meow!", "Woof!". The array is declared as Animal[], so each element is a superclass reference. The loop does not need to check types because polymorphism handles it automatically: the JVM inspects the actual object behind each reference and calls the appropriate overridden speak() method. This is exactly why polymorphism is so powerful — it eliminates the need for if/else chains checking instanceof. If you later add a Bird to the array, the loop works without changes.

Question 4: APPLIED

You are building a payroll system. The superclass Employee has a method calculatePay(). Subclasses HourlyEmployee and SalariedEmployee override this method. Write pseudocode (or explain in detail) how you would use a superclass reference to process a list of employees without knowing each employee's specific type.
Show answer
You would create an array or ArrayList of type Employee: Employee[] staff = { new HourlyEmployee(...), new SalariedEmployee(...), ... }. Then, in a loop, you call staff[i].calculatePay() for each element. Because calculatePay() is defined in Employee and overridden in each subclass, the JVM automatically calls the correct version. HourlyEmployee might calculate hours × rate, while SalariedEmployee might return annualSalary / 24. The calling code stays the same regardless of how many employee types you add in the future, demonstrating real-world extensibility.

Question 5: CRITICAL THINKING

A student writes: Animal a = new Dog(); a.fetch(); where fetch() exists only in Dog. The student argues, "But a IS a Dog, so this should work!" Explain why the compiler rejects this, discuss whether this is a flaw in Java's design, and describe how the student could safely fix the code.
Show answer
The compiler only sees the declared type (Animal), and Animal does not have a fetch() method, so it refuses to compile. This is not a flaw — it is a deliberate safety feature. If the compiler allowed this, what would happen if 'a' were later reassigned to a Cat? Cat has no fetch() method, and the program would crash. By enforcing declared-type checking, Java catches these errors before the program runs. The student can fix this in two ways: (1) cast the variable — ((Dog) a).fetch() — which tells the compiler 'I guarantee this is a Dog', or (2) use the instanceof operator first to check safely: if (a instanceof Dog) { ((Dog) a).fetch(); }. This shows that type safety and polymorphism work together, not against each other.

SUMMARY

Lesson Summary

Summary

Polymorphism allows a single superclass reference to point to any subclass object. The declared type (the superclass) determines which methods the compiler will let you call, while the actual (runtime) type (the subclass) determines which overridden version of a method the JVM executes. This two-phase process is called dynamic method dispatch.

Three conditions must be met for polymorphism to work: inheritance (a subclass extends a superclass), method overriding (the subclass redefines a superclass method), and a superclass reference variable holding the subclass object. Polymorphism makes code flexible, extensible, and clean — you can add new subclasses without modifying existing logic. Remember that polymorphism applies to methods only, not to instance variables, and that subclass-specific methods require casting to access through a superclass reference.

Varsity Tutors • Java • Polymorphism — Use polymorphism with superclass references (conceptual)