What is polymorphism in Java, and what is the difference between compile-time and runtime polymorphism?
Polymorphism (Greek for "many forms") is the OOP principle that lets a single method name or reference type exhibit different behavior depending on context. Java provides two kinds: compile-time polymorphism through method overloading and runtime polymorphism through method overriding.
In test automation you use it daily: WebDriver driver = new ChromeDriver(); is runtime polymorphism — the JVM invokes the methods of the actual ChromeDriver object at runtime, even though the reference is declared as the WebDriver interface.
Compile-time (static) polymorphism — achieved by overloading: same method name, different parameter lists in one class. The compiler binds the call from the declared (compile-time) types of the arguments, which is why it is also called static binding or early binding.
Runtime (dynamic) polymorphism — achieved by overriding: a subclass redefines an inherited method with the same signature. The JVM performs dynamic method dispatch, invoking the method of the actual object, not of the reference type.
Rules to remember — overloading cannot differ only by return type; overriding requires the same signature, a compatible (covariant) return type, and access no more restrictive than the parent.
static,private, andfinalmethods cannot be overridden — static methods are hidden instead.Why frameworks rely on it — Selenium's
findElementworks against anyWebDriverimplementation, and TestNG's overloadedAssert.assertEqualsvariants accept different argument types — both are polymorphism in action.
Key terms: polymorphism, method overloading, method overriding, static binding, dynamic method dispatch, upcasting, @Override
public class PolymorphismDemo {
// Compile-time polymorphism: overloaded methods
static int add(int a, int b) {
return a + b;
}
static double add(double a, double b) {
return a + b;
}
public static void main(String[] args) {
System.out.println(add(2, 3)); // Output: 5
System.out.println(add(2.5, 3.5)); // Output: 6.0
// Runtime polymorphism: parent reference, child object
Browser browser = new Chrome(); // upcasting
browser.launch(); // Output: Launching Chrome browser
browser = new Firefox();
browser.launch(); // Output: Launching Firefox browser
}
}
class Browser {
void launch() {
System.out.println("Launching a generic browser");
}
}
class Chrome extends Browser {
@Override
void launch() {
System.out.println("Launching Chrome browser");
}
}
class Firefox extends Browser {
@Override
void launch() {
System.out.println("Launching Firefox browser");
}
}The two add methods demonstrate compile-time polymorphism: the compiler selects the int or double version purely from the compile-time argument types, before the program ever runs. The Browser hierarchy demonstrates runtime polymorphism: even though the reference type is Browser, the JVM dispatches launch() to the actual object (Chrome, then Firefox) at runtime.
This mirrors how Selenium code written against the WebDriver interface runs unchanged on ChromeDriver or FirefoxDriver.
Lead with the automation example WebDriver driver = new ChromeDriver() and name the mechanisms: static binding for overloading, dynamic method dispatch for overriding. Expect follow-ups like "Can you override a static or private method?" (no — static methods are hidden and private methods are not inherited), "Can overloading differ only by return type?" (no), and "What are covariant return types?".
A classic mistake is calling overloading runtime polymorphism or mixing up which one the JVM resolves.