⏱️ 23 min read • Beginner Level • Lesson 59
Completed on . You can revise this lesson or continue to the next topic.
Polymorphism in Java allows one method name, parent type, or interface type to represent multiple forms of behavior.
Java supports compile-time polymorphism through method overloading and runtime polymorphism through method overriding. Polymorphism helps developers write flexible code that depends on common contracts while concrete objects provide specialized behavior.
The word polymorphism means “many forms.” In Java, the same operation can behave differently depending on the method arguments known at compile time or the actual object selected at runtime.
The action “notify a user” can take several forms: email, SMS, or a mobile push notification. The requested action remains the same, but the actual behavior depends on the selected notification service.
send(message)
Email, SMS, and push services.
Each object sends the message in its own way.
| Type | Achieved Through | Decision Time |
|---|---|---|
| Compile-time polymorphism | Method overloading | The compiler selects the matching method. |
| Runtime polymorphism | Method overriding and upcasting | The JVM selects the overridden instance method. |
Compile-time polymorphism occurs through method overloading. Multiple methods share the same name but have different parameter lists. The compiler selects the most applicable method based on the arguments.
class Calculator {
int add(int first, int second) {
return first + second;
}
double add(double first, double second) {
return first + second;
}
int add(int first, int second, int third) {
return first + second + third;
}
public static void main(String[] args) {
Calculator calculator = new Calculator();
System.out.println(calculator.add(10, 20));
System.out.println(calculator.add(2.5, 3.5));
System.out.println(calculator.add(1, 2, 3));
}
}
Output:
30 6.0 6
add() is overloaded three times with different parameter lists (types and counts).calculator.add(10, 20) is called, the compiler resolves it to the method accepting two ints.calculator.add(2.5, 3.5) is called, the compiler resolves it to the method accepting two doubles.Runtime polymorphism occurs when a child class overrides an inherited instance method and a parent reference refers to a child object. The actual object decides which overridden method runs.
class Animal {
void makeSound() {
System.out.println("Animal makes a sound");
}
}
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Dog says Woof");
}
}
class Cat extends Animal {
@Override
void makeSound() {
System.out.println("Cat says Meow");
}
}
class AnimalExample {
static void playSound(Animal animal) {
animal.makeSound();
}
public static void main(String[] args) {
playSound(new Dog());
playSound(new Cat());
}
}
Output:
Dog says Woof Cat says Meow
playSound() accepts the common parent type.Dog or Cat object can be passed to it.Upcasting stores a child object in a parent-class or interface reference. It is usually implicit and safe because every child object is also an instance of its parent type.
class Vehicle {
void move() {
System.out.println("Vehicle moves");
}
}
class Car extends Vehicle {
@Override
void move() {
System.out.println("Car drives");
}
void openTrunk() {
System.out.println("Trunk opened");
}
}
class UpcastingExample {
public static void main(String[] args) {
Vehicle vehicle = new Car();
vehicle.move();
// vehicle.openTrunk(); // Not visible through Vehicle reference
}
}
Output:
Car drives
Vehicle vehicle = new Car(); is an example of upcasting. A Car object is stored in a Vehicle reference.Vehicle reference, we can call vehicle.move(). The overridden Car.move() executes at runtime.vehicle.openTrunk() because the compiler only allows access to methods defined in the Vehicle reference type.Dynamic method dispatch is the runtime process used to select an overridden instance method. Java examines the actual object rather than only the reference type.
Animal animalnew Dog()Dog.makeSound()Interface references make polymorphic designs possible even when implementations are otherwise unrelated. Each class agrees to the same contract.
interface PaymentMethod {
void pay(double amount);
}
class CardPayment implements PaymentMethod {
@Override
public void pay(double amount) {
System.out.println("Card payment: ₹" + amount);
}
}
class UpiPayment implements PaymentMethod {
@Override
public void pay(double amount) {
System.out.println("UPI payment: ₹" + amount);
}
}
class Checkout {
static void process(PaymentMethod method, double amount) {
method.pay(amount);
}
public static void main(String[] args) {
process(new CardPayment(), 1500);
process(new UpiPayment(), 800);
}
}
Output:
Card payment: ₹1500.0 UPI payment: ₹800.0
PaymentMethod is a common interface used as the reference type in process().CardPayment and UpiPayment) are passed to it.pay() behavior changes based on which object is actually passed, demonstrating runtime polymorphism using interfaces.An abstract class can define common state and behavior while subclasses provide specialized implementations. The abstract parent type can then be used polymorphically.
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;
}
}
Shape is an abstract class defining the area() contract.Circle and Rectangle provide concrete mathematical implementations for area().printArea(Shape s) that calculates the area without knowing the exact shape at compile time!Instance methods are dynamically dispatched, but fields are resolved using the reference type. A child field with the same name hides the parent field rather than overriding it.
class Parent {
String name = "Parent";
String getName() {
return name;
}
}
class Child extends Parent {
String name = "Child";
@Override
String getName() {
return name;
}
public static void main(String[] args) {
Parent value = new Child();
System.out.println(value.name);
System.out.println(value.getName());
}
}
Output:
Parent Child
value.name outputs "Parent" because the reference type is Parent.value.getName() outputs "Child" because the actual object is Child.Static methods are associated with classes, not objects. A child static method with the same signature hides the parent method; it does not participate in runtime overriding.
class Parent {
static void show() {
System.out.println("Parent static method");
}
}
class Child extends Parent {
static void show() {
System.out.println("Child static method");
}
public static void main(String[] args) {
Parent value = new Child();
value.show();
}
}
Output:
Parent static method
value is a Parent reference, Parent.show() is executed, not Child.show().
Downcasting converts a parent reference back to a child reference. It should be used only when the actual object is compatible; otherwise, Java throws ClassCastException.
class Vehicle {
void move() {
System.out.println("Vehicle moves");
}
}
class Car extends Vehicle {
void openTrunk() {
System.out.println("Trunk opened");
}
}
class DowncastingExample {
public static void main(String[] args) {
Vehicle vehicle = new Car();
if (vehicle instanceof Car car) {
car.openTrunk();
}
}
}
Output:
Trunk opened
Vehicle vehicle = new Car(); upcasts the object, hiding openTrunk().if (vehicle instanceof Car car) safely checks if the object is actually a Car, and if so, downcasts it into the car variable (using Java 16+ pattern matching).openTrunk() method.| Feature | Overloading | Overriding |
|---|---|---|
| Polymorphism type | Compile-time | Runtime |
| Requirement | Same method name, different parameter list | Inheritance plus compatible method signature |
| Selection | Based on compile-time argument types | Based on the actual object for instance methods |
| Return type | Cannot differ alone | Same or covariant for reference return types |
| Static methods | Can be overloaded | Are hidden, not overridden |
| Private methods | Can be overloaded in their class | Cannot be overridden |
One operation works with many compatible object types.
Client code depends on a parent type or interface.
New implementations can be added with fewer client changes.
Production dependencies can be replaced with test implementations.
@Override to help the compiler detect signature mistakes.if/switch chains based on object type.@Override and accidentally creating a different method.Test your understanding with a quick quiz
Topic: Polymorphism | Language: Java
class Parent {
static void show() {
System.out.println("Parent");
}
}
class Child extends Parent {
static void show() {
System.out.println("Child");
}
}
class Test {
public static void main(String[] args) {
Parent value = new Child();
value.show();
}
}class Printer {
void print(int value) {
System.out.println("int");
}
void print(double value) {
System.out.println("double");
}
public static void main(String[] args) {
new Printer().print(10);
}
}class Animal {
void sound() {
System.out.println("Animal");
}
}
class Dog extends Animal {
@Override
void sound() {
System.out.println("Dog");
}
class Test {
public static void main(String[] args) {
Animal animal = new Dog();
animal.sound();
}
}class Parent {
String name = "Parent";
}
class Child extends Parent {
String name = "Child";
}
class Test {
public static void main(String[] args) {
Parent value = new Child();
System.out.println(value.name);
}
}Great job! Continue learning Java OOP step by step.
Mark this lesson as completed to track your learning progress. To keep track across devices, please login and save your learning progress.
Discussion
Ask questions, share suggestions, or discuss this lesson.