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.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
