Polymorphism in Java

One reference type, many possible behaviours. Polymorphism is what lets code work with a supertype and stay correct as new subtypes appear.

Definition

Polymorphism means a single reference type can refer to objects of many types, and the behaviour that runs depends on the actual object. Literally, one name with many forms.

The two kinds

Compile time polymorphismRuntime polymorphism
Achieved byMethod overloadingMethod overriding
ResolvedAt compile timeAt runtime
Based onDeclared argument typesThe actual object
Also calledStatic bindingDynamic binding

When people say "polymorphism" without qualification they almost always mean the runtime kind.

Runtime polymorphism in practice

abstract class Shape {
    abstract double area();
}

class Circle extends Shape {
    private final double radius;
    Circle(double radius) { this.radius = radius; }
    @Override double area() { return Math.PI * radius * radius; }
}

class Rectangle extends Shape {
    private final double width;
    private final double height;
    Rectangle(double width, double height) { this.width = width; this.height = height; }
    @Override double area() { return width * height; }
}
List<Shape> shapes = List.of(new Circle(2), new Rectangle(3, 4));

double total = 0;
for (Shape shape : shapes) {
    total += shape.area();      // the right implementation each time
}
System.out.printf("Total area: %.2f%n", total);
The loop knows nothing about circles or rectangles. Add a Triangle tomorrow and this code does not change at all. That is the practical value of polymorphism: existing code keeps working as new types arrive.

Upcasting and downcasting

Shape shape = new Circle(2);          // upcast, implicit and always safe

// The compiler now only offers Shape members
// shape.radius;                       // not visible

if (shape instanceof Circle circle) {  // test and cast in one step
    System.out.println("A circle was supplied");
}

Upcasting narrows what the compiler will let you call, but it never changes the object. The override still runs, which is the whole point.

Fields are not polymorphic

class Parent {
    String label = "parent";
    String describe() { return "parent method"; }
}

class Child extends Parent {
    String label = "child";
    @Override String describe() { return "child method"; }
}

Parent p = new Child();
System.out.println(p.label);        // parent  - fields use the declared type
System.out.println(p.describe());   // child method - methods use the object

Field access is resolved at compile time and is hidden, not overridden. This is a common interview question, and it is also a good reason to keep fields private.

Polymorphism through interfaces

interface Exporter {
    String export(Note note);
}

class JsonExporter implements Exporter {
    @Override public String export(Note note) { return "{...}"; }
}

class CsvExporter implements Exporter {
    @Override public String export(Note note) { return "id,title"; }
}

class ExportService {

    private final Exporter exporter;    // depends on the abstraction

    ExportService(Exporter exporter) {
        this.exporter = exporter;
    }

    String run(Note note) {
        return exporter.export(note);
    }
}

The service is written against the interface, so the concrete exporter can be chosen at runtime and swapped in a test. Interface polymorphism is the form used most in real code, because it does not require a class hierarchy.

How the JVM does it

Each class has a table of method implementations. A virtual call looks up the method in the table belonging to the object, not the reference. Modern JVMs then optimise heavily: when only one implementation has ever been seen at a call site, the JIT compiler can inline it and remove the lookup entirely.

Common mistakes

  • Expecting fields to behave like methods under polymorphism.
  • Writing a chain of instanceof tests instead of letting the objects decide. Each new type then means editing that chain.
  • Casting down to reach a subtype method, which usually means the abstraction is wrong.
  • Assuming an overloaded method is chosen from the runtime type.
  • Declaring variables as the concrete class instead of the interface.

Best practices

  • Declare variables, parameters and return types using the most general type that suffices.
  • Replace type checking chains with an overridden method, or with a sealed hierarchy and pattern matching where that reads better.
  • Keep the supertype contract meaningful, so every subtype can honour it.
  • Prefer interface polymorphism to inheritance when there is no shared state.

Practice

  1. Add a Triangle to the shape example and confirm that the totalling loop needs no change.
  2. Predict both lines of the field hiding example, then make the fields private and describe what changes.
  3. Rewrite an if (x instanceof A) ... else if (x instanceof B) chain using an overridden method.
  4. Why does List<Shape> shapes allow both circles and rectangles, and what does the compiler permit you to call on an element?
  5. Explain the difference between overloading and overriding using a single short program.

Conclusion

Polymorphism lets you write code once against a general type and have it stay correct as new specific types appear. Methods are dispatched on the object, fields are not, and interfaces are usually the cleanest way to obtain it.

Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All Java notes →

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.