Fundamentals 6 min read

Understanding Java Polymorphism: Concepts, Implementation, and Common Pitfalls

This article explains Java polymorphism by illustrating inheritance hierarchies, runtime method binding, and the differences between static and dynamic dispatch, while providing concrete code examples that demonstrate how method overriding, field access, and down‑casting behave in practice.

Java One
Java One
Java One
Understanding Java Polymorphism: Concepts, Implementation, and Common Pitfalls

This chapter introduces the concept and features of polymorphism in the Java language.

1. What Is Polymorphism

An example shows three instrument types— Wind, Brass, and Stringed —all extending a base class Instrument. The base class defines an enum Note and a method play(Note n) that prints the note.

public enum Note { MIDDLE_C, C_SHARP, B_FLAT; }
class Instrument {
    public void play(Note n) {
        print("Instrument.play() " + n);
    }
}

The subclass Wind overrides play to provide its own implementation.

class Wind extends Instrument {
    public void play(Note n) {
        print("Wind.play() " + n);
    }
}

A utility class calls the method via a reference of type Instrument:

public class Music {
    public static void tune(Instrument i) {
        i.play(Note.MIDDLE_C);
    }
    public static void main(String[] args) {
        Wind flute = new Wind();
        tune(flute);
    }
}

The output demonstrates dynamic dispatch: Wind.play() MIDDLE_C. The tune method accepts any Instrument subclass, and the actual method executed depends on the runtime type, which is the essence of polymorphism.

2. How Polymorphism Is Implemented

Java performs method binding at runtime (late binding). Unlike C, which resolves calls at compile time (early binding), Java decides which method to invoke based on the actual object type when the program runs. All non‑static, non‑final methods are subject to this dynamic dispatch; marking a method static or final disables it.

Characteristics of Polymorphism

Final methods (including private methods) are not polymorphic.

Static methods are not polymorphic because they belong to the class, not an instance.

Only instance methods exhibit polymorphism; fields do not.

A classic example shows that a private method is treated as final and is not overridden:

public class PrivateOverride {
    private void f() { print("private f()"); }
    public static void main(String[] args) {
        PrivateOverride po = new Derived();
        po.f();
    }
}
class Derived extends PrivateOverride {
    public void f() { print("public f()"); }
}

The call invokes the base class's private method, not the derived one, because the private method is not visible to the subclass.

Another example contrasts field access and method overriding:

class Super {
    public int field = 0;
    public int getField() { return field; }
}
class Sub extends Super {
    public int field = 1;
    public int getField() { return super.field; }
}
public class FieldAccess {
    public static void main(String[] args) {
        Super sup = new Sub();
        System.out.println("sup.field = " + sup.field + ", sup.getField() = " + sup.getField());
    }
}

Output: sup.field = 0, sup.getField() = 1. The field value comes from the reference type ( Super), while the overridden method exhibits polymorphic behavior.

3. Downcasting

Upcasting discards specific type information, while downcasting attempts to recover it. Downcasting is unsafe; Java performs runtime type checks and throws a ClassCastException if the actual object is not of the expected subclass. This runtime type identification (RTTI) ensures type safety during explicit casts.

JavaOOPPolymorphismInheritanceMethod OverridingRuntime Binding
Java One
Written by

Java One

Sharing common backend development knowledge.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.