Polymorphism

Su Suriya Ravichandran Updated 14 Nov 2026
9 min read ·Lesson 52 of 133

Polymorphism ("many forms") is the ability of one piece of code to work with objects of different types, with each object responding in its own way. A method that accepts a Shape can be given a Circle, a Rectangle, or a Triangle; calling area() runs the correct calculation for each. Polymorphism is what makes object-oriented programs extensible: new types can be added without changing the code that uses them.

A First Example

public abstract class Notification {
    protected final String recipient;

    protected Notification(String recipient) {
        this.recipient = recipient;
    }

    public abstract void send(String message);
}

public class EmailNotification extends Notification {
    public EmailNotification(String address) { super(address); }

    @Override
    public void send(String message) {
        System.out.println("Email to " + recipient + ": " + message);
    }
}

public class SmsNotification extends Notification {
    public SmsNotification(String phone) { super(phone); }

    @Override
    public void send(String message) {
        String shortMsg = message.length() > 40 ? message.substring(0, 37) + "..." : message;
        System.out.println("SMS to " + recipient + ": " + shortMsg);
    }
}
List<Notification> channels = List.of(
    new EmailNotification("ada@example.com"),
    new SmsNotification("+919000000000")
);

for (Notification n : channels) {
    n.send("Your Java course certificate is ready to download.");
}

Output:

Email to ada@example.com: Your Java course certificate is ready to download.
SMS to +919000000000: Your Java course certificate is ready...

The loop calls n.send(...) on a variable of type Notification and never checks which kind it is. Each object runs its own send: the email version sends the full text, while the SMS version keeps the first 37 characters and adds an ellipsis. Adding a WhatsAppNotification class later requires no change to the loop.

Declared Type Versus Actual Type

Every reference involves two types:

Notification n = new SmsNotification("+919000000000");
//  declared type ▲          ▲ actual (runtime) type
DeterminedControls
Declared type (Notification)At compile time, from the variable's declarationWhich methods you are allowed to call
Actual type (SmsNotification)At runtime, from the object that was createdWhich implementation of an overridden method runs
Notification n = new SmsNotification("+919000000000");
n.send("Hi");          // allowed (Notification declares send); runs SmsNotification.send
n.shorten("Hi");       // compile error, even if SmsNotification had such a method

The compiler only knows that n is some Notification, so it only allows calls that every Notification supports. At runtime, the JVM looks at the real object and dispatches to its override. This runtime selection is called dynamic dispatch or late binding.

Runtime Polymorphism: Method Overriding

Runtime polymorphism comes from overriding: subclasses provide their own implementations of methods declared in a superclass or interface.

public class Shape {
    public double area() { return 0; }
}

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

public class Square extends Shape {
    private final double side;
    public Square(double side) { this.side = side; }
    @Override public double area() { return side * side; }
}
static double totalArea(List<Shape> shapes) {
    double sum = 0;
    for (Shape s : shapes) {
        sum += s.area();       // which area()? decided per object at runtime
    }
    return sum;
}

System.out.printf("%.2f%n", totalArea(List.of(new Circle(1), new Square(2))));   // 7.14

totalArea is written once against Shape. It handles every present and future subclass correctly.

What is and is not polymorphic

Only instance methods are dispatched dynamically. Other members are resolved from the declared type:

MemberResolved by
Overridden instance methodsActual (runtime) type
FieldsDeclared type
static methodsDeclared type
private methodsNot inherited, so never overridden
class Parent {
    String label = "parent";
    static String kind() { return "Parent.kind"; }
    String name() { return "Parent.name"; }
}

class Child extends Parent {
    String label = "child";                      // hides the field, does not override it
    static String kind() { return "Child.kind"; }
    @Override String name() { return "Child.name"; }
}

Parent p = new Child();
System.out.println(p.label);    // parent      (field: declared type)
System.out.println(p.kind());   // Parent.kind (static: declared type)
System.out.println(p.name());   // Child.name  (instance method: runtime type)

This is one more reason to keep fields private and access them through methods: methods behave polymorphically, fields do not.

Polymorphism Through Interfaces

Interfaces provide polymorphism without requiring a common parent class. Any class can implement an interface, regardless of its position in a class hierarchy:

public interface Discount {
    double apply(double amount);
}

public class PercentageDiscount implements Discount {
    private final double percent;
    public PercentageDiscount(double percent) { this.percent = percent; }
    public double apply(double amount) { return amount * (1 - percent / 100); }
}

public class FlatDiscount implements Discount {
    private final double off;
    public FlatDiscount(double off) { this.off = off; }
    public double apply(double amount) { return Math.max(0, amount - off); }
}

public class NoDiscount implements Discount {
    public double apply(double amount) { return amount; }
}
static double checkout(double cartTotal, Discount discount) {
    return discount.apply(cartTotal);
}

System.out.println(checkout(2000, new PercentageDiscount(10)));   // 1800.0
System.out.println(checkout(2000, new FlatDiscount(250)));        // 1750.0
System.out.println(checkout(2000, new NoDiscount()));             // 2000.0

checkout depends only on the Discount interface. The concrete discount can be chosen at runtime (from a coupon code, a promotion calendar, or a database), and a new kind of discount is just another class. Because Discount has a single abstract method, a lambda can implement it too:

System.out.println(checkout(2000, amount -> amount * 0.5));       // 1000.0

Programming against interfaces like this ("program to an interface, not an implementation") is the foundation of testable, loosely coupled designs. In tests, a fake implementation can replace a real payment gateway or email sender without changing the code under test.

Compile-Time Polymorphism: Method Overloading

Method overloading (several methods with the same name but different parameter lists) is sometimes called compile-time or static polymorphism:

public class Printer {
    void print(int value)    { System.out.println("int: " + value); }
    void print(double value) { System.out.println("double: " + value); }
    void print(String value) { System.out.println("String: " + value); }
}

Printer pr = new Printer();
pr.print(5);        // int: 5
pr.print(5.0);      // double: 5.0
pr.print("five");   // String: five

The compiler chooses the overload from the declared types of the arguments, before the program runs. This differs from overriding:

void handle(Object o) { System.out.println("Object"); }
void handle(String s) { System.out.println("String"); }

Object x = "hello";
handle(x);          // prints "Object": chosen from the declared type of x

Even though x refers to a String at runtime, the overload was fixed at compile time from the type Object. Overloading and overriding each have their own lessons.

OverloadingOverriding
Also calledCompile-time / static polymorphismRuntime / dynamic polymorphism
Methods involvedSame name, different parameters, usually in one classSame signature, in parent and child classes
Chosen byDeclared argument types, at compile timeActual object type, at runtime

Upcasting and Downcasting

Assigning a subclass object to a superclass variable is upcasting. It is always safe and automatic:

Shape s = new Circle(3);        // upcast

Getting the specific type back is downcasting. It requires an explicit cast and can fail at runtime:

Circle c = (Circle) s;          // fine: s really is a Circle
Square q = (Square) s;          // ClassCastException at runtime

Pattern matching with instanceof tests and casts safely in one step:

if (s instanceof Circle circle) {
    System.out.println("Radius-based shape with area " + circle.area());
}

Frequent downcasting is a sign that polymorphism is not being used. If code repeatedly asks "which subclass is this?" and then branches, the behavior usually belongs in an overridden method instead:

// Fragile: must be updated for every new shape
if (s instanceof Circle c) { ... } else if (s instanceof Square q) { ... }

// Polymorphic: each shape knows how to do it
s.area();

One modern exception is sealed hierarchies with pattern-matching switch, where the compiler checks that every permitted subtype is handled. That approach suits data-like types whose set of variants is fixed and known.

Benefits in Real Systems

  • Extensibility. New types plug into existing code. A payment system written against a PaymentMethod interface supports a new provider by adding one class.
  • Reduced conditionals. Replacing long if/switch chains on a "type" field with overridden methods keeps each variant's logic in its own class.
  • Testability. Code that depends on interfaces can be tested with fake implementations.
  • Frameworks. Java frameworks rely on polymorphism everywhere: List is implemented by ArrayList and LinkedList; Spring injects implementations of interfaces; servlets override doGet; JUnit runs any class following its conventions.

Practical Example: Payment Processing

import java.util.List;

public class Checkout {
    interface PaymentMethod {
        String name();
        double fee(double amount);
        boolean pay(double amount);
    }

    static class Card implements PaymentMethod {
        public String name() { return "Card"; }
        public double fee(double amount) { return amount * 0.02; }
        public boolean pay(double amount) {
            System.out.printf("  Charging card %.2f%n", amount);
            return true;
        }
    }

    static class Upi implements PaymentMethod {
        public String name() { return "UPI"; }
        public double fee(double amount) { return 0; }
        public boolean pay(double amount) {
            System.out.printf("  Sending UPI request for %.2f%n", amount);
            return true;
        }
    }

    static class Wallet implements PaymentMethod {
        private double balance;
        Wallet(double balance) { this.balance = balance; }

        public String name() { return "Wallet"; }
        public double fee(double amount) { return 5; }
        public boolean pay(double amount) {
            if (amount > balance) {
                System.out.println("  Wallet balance too low");
                return false;
            }
            balance -= amount;
            System.out.printf("  Wallet debited %.2f, left %.2f%n", amount, balance);
            return true;
        }
    }

    static boolean process(double orderTotal, PaymentMethod method) {
        double payable = orderTotal + method.fee(orderTotal);
        System.out.printf("%s: payable %.2f%n", method.name(), payable);
        return method.pay(payable);
    }

    public static void main(String[] args) {
        List<PaymentMethod> methods = List.of(new Card(), new Upi(), new Wallet(1000));
        for (PaymentMethod m : methods) {
            boolean ok = process(1200, m);
            System.out.println("  Result: " + (ok ? "success" : "failed"));
        }
    }
}

Output:

Card: payable 1224.00
  Charging card 1224.00
  Result: success
UPI: payable 1200.00
  Sending UPI request for 1200.00
  Result: success
Wallet: payable 1205.00
  Wallet balance too low
  Result: failed

process is written once against the PaymentMethod interface. It asks each method for its name and fee and then calls pay, and each implementation supplies its own rules: a percentage fee for cards, no fee for UPI, a flat fee and a balance check for the wallet. Supporting a new payment provider means writing one more class that implements PaymentMethod; process and every other piece of checkout code stay exactly as they are.

0 Comments

Reviewed before they appear

No comments yet.

Java
Java Introduction Java Features Java Syntax Java Comments Java Statements Java Output Java Variables Java Data Types Java Type Casting Java Operators Java Strings Java Numbers Java Math Java Booleans Java User Input Java Conditions Java If Else Java Switch Java While Loop Java For Loop Java Break Java Continue Java Arrays Java Methods Java Parameters Java Return Values Java Scope Java Recursion Install Java Install JDK Java IDEs Java JDK Java JRE Java JVM Java Compiler Java File Compile Java Run Java JShell Basics Java Packages Java OOP Java Classes Java Objects Class Methods Constructors Constructor Overload this Keyword static Keyword final Keyword Encapsulation Inheritance Polymorphism Abstraction Access Modifiers super Keyword Method Overload Method Override Instanceof Operator Nested Classes Inner Classes Anonymous Classes Enums Records Array Basics Array Length Loop Arrays Multidimensional Arrays Array Copy Array Sorting Array Search ArrayList LinkedList HashSet TreeSet HashMap TreeMap Queue Interface Deque Interface Stack Class Iterator List Interface Set Interface Map Interface Collections Class Comparable Comparator Generics Basics Generic Classes Generic Methods Wildcards String Basics String Length String Methods String Concatenation String Compare String Search String Replace String Split String Format StringBuilder StringBuffer Text Blocks Regular Expressions Number Types Integer Class Float Class Double Class BigInteger BigDecimal Math Class Random Numbers Number Format Date Basics LocalDate LocalTime LocalDateTime ZonedDateTime Date Formatting Date Parsing Period Class Duration Class Java Instant Class Java Exception Basics Java try...catch Java Multiple Catch Blocks Java finally Block Java throw Keyword Java throws Keyword Java Checked Exceptions Java Unchecked Exceptions Java Custom Exceptions Java try-with-resources Java Exception Chaining
Ask about this post
AI Ask about this post

Ask questions about Polymorphism and get answers drawn from it.

Signed-in readers only.