Polymorphism in Java
Learn Polymorphism in Java in detail, including compile-time and runtime polymorphism, method overloading, method overriding, dynamic method dispatch, upcasting, covariant return types, and practical examples.
Introduction
Polymorphism is one of the fundamental principles of Object-Oriented Programming. It allows the same method call, reference type, or interface to represent different forms of behavior.
class Animal {
void sound() {
System.out.println(
"Animal makes a sound"
);
}
}
class Dog extends Animal {
@Override
void sound() {
System.out.println(
"Dog barks"
);
}
}
class Cat extends Animal {
@Override
void sound() {
System.out.println(
"Cat meows"
);
}
}Animal animal;
animal = new Dog();
animal.sound();
animal = new Cat();
animal.sound();Dog barks
Cat meowsThe same Animal reference is used with different child objects. The sound() method behaves differently depending on the actual object.
- What polymorphism is.
- Why polymorphism is useful.
- The types of polymorphism in Java.
- What compile-time polymorphism is.
- How method overloading works.
- Rules of method overloading.
- How automatic type promotion affects overloaded methods.
- What ambiguous method calls are.
- What runtime polymorphism is.
- How method overriding enables runtime polymorphism.
- The difference between reference type and object type.
- What dynamic method dispatch is.
- How upcasting enables polymorphism.
- The limitations of parent references.
- How downcasting works.
- How instanceof works.
- How pattern matching with instanceof works.
- How to create polymorphic arrays.
- How to use polymorphic method parameters.
- How to use polymorphic return types.
- What covariant return types are.
- How abstract classes support polymorphism.
- How interfaces support polymorphism.
- How polymorphism supports dependency injection.
- Why static methods do not provide runtime polymorphism.
- Why fields are not polymorphic.
- The difference between overloading and overriding.
- The difference between early and late binding.
- How to design flexible applications using polymorphism.
What is Polymorphism?
The word polymorphism means many forms. In Java, polymorphism allows one method name, parent type, abstract class, or interface to represent different implementations and behaviors.
POLY
Many
MORPH
Forms
POLYMORPHISM
Many FormsSAME METHOD CALL
sound()
│
▼
┌──────────────┐
│ Actual Object │
└──────────────┘
/ | \
▼ ▼ ▼
Dog Cat Bird
│ │ │
▼ ▼ ▼
Bark Meow Chirp- One name can have multiple forms.
- One parent reference can represent different child objects.
- The same method call can produce different behavior.
- The actual implementation can be selected automatically.
- Code can depend on abstractions instead of specific classes.
Why Use Polymorphism?
Flexible Code
The same code can work with many different object types.
Extensibility
New implementations can often be added without changing existing code.
Loose Coupling
Code can depend on parent classes, abstract classes, or interfaces.
Reusable Logic
Common processing logic can work with multiple implementations.
Dynamic Behavior
The correct overridden method can be selected at runtime.
Testability
Implementations can be replaced with test versions more easily.
Real-World Analogy
Consider a payment system. A customer can make a payment using a credit card, UPI, or digital wallet. The action is the same—pay—but each payment method performs the operation differently.
PAYMENT
pay()
/ | \
/ | \
▼ ▼ ▼
Credit Card UPI Wallet
│ │ │
▼ ▼ ▼
Card Logic UPI Logic Wallet LogicThe application can work with the general Payment type while each implementation decides how the payment is processed.
Types of Polymorphism
POLYMORPHISM
├── Compile-Time Polymorphism
│
│ └── Method Overloading
│
└── Runtime Polymorphism
└── Method Overriding- Compile-time polymorphism is generally achieved through method overloading.
- Runtime polymorphism is achieved through method overriding and dynamic method dispatch.
Compile-Time Polymorphism
Compile-time polymorphism occurs when the compiler determines which method should be called. Method overloading is the main example.
class Calculator {
int add(int a, int b) {
return a + b;
}
double add(
double a,
double b
) {
return a + b;
}
}Calculator calculator =
new Calculator();
calculator.add(10, 20);
calculator.add(10.5, 20.5);The compiler selects the correct add() method by examining the arguments.
Method Overloading
Method overloading allows multiple methods in the same class to have the same name with different parameter lists.
class Printer {
void print(String text) {
System.out.println(text);
}
void print(int number) {
System.out.println(number);
}
void print(
String text,
int copies
) {
for (
int i = 0;
i < copies;
i++
) {
System.out.println(text);
}
}
}Rules of Method Overloading
- Overloaded methods must have the same method name.
- The parameter list must be different.
- The number of parameters can be different.
- The parameter types can be different.
- The order of parameter types can be different.
- Return type alone cannot overload a method.
- Access modifiers may be different.
- Methods may throw different exceptions.
- Static methods can be overloaded.
- Final methods can be overloaded.
- Private methods can be overloaded.
Valid Method Overloading
void display() {
}
void display(int value) {
}
void display(String value) {
}
void display(
int first,
int second
) {
}
void display(
String text,
int number
) {
}
void display(
int number,
String text
) {
}Invalid Method Overloading
int calculate(int value) {
return value;
}
double calculate(int value) {
return value;
}
// Compilation errorBoth methods have the same name and the same parameter list. Changing only the return type does not create a different method signature for overloading.
Overloading by Number of Parameters
class Calculator {
int add(int a, int b) {
return a + b;
}
int add(
int a,
int b,
int c
) {
return a + b + c;
}
}Overloading by Parameter Types
class Converter {
void convert(int value) {
System.out.println(
"Integer: " + value
);
}
void convert(double value) {
System.out.println(
"Double: " + value
);
}
void convert(String value) {
System.out.println(
"String: " + value
);
}
}Overloading by Parameter Order
void show(
String name,
int age
) {
}
void show(
int age,
String name
) {
}Return Type and Overloading
The return type may be different in overloaded methods, but only when the parameter lists are also different.
int process(int value) {
return value;
}
double process(double value) {
return value;
}Static Method Overloading
class MathUtility {
static int max(
int a,
int b
) {
return a > b ? a : b;
}
static double max(
double a,
double b
) {
return a > b ? a : b;
}
}Static methods can be overloaded because overloading is resolved using method signatures at compile time.
Constructor Overloading
class Student {
private String name;
private int age;
Student() {
this(
"Unknown",
0
);
}
Student(String name) {
this(
name,
0
);
}
Student(
String name,
int age
) {
this.name = name;
this.age = age;
}
}Automatic Type Promotion
When an exact overloaded method is unavailable, Java may promote an argument to a compatible wider type.
class Demo {
void show(int value) {
System.out.println(
"int method"
);
}
}
Demo demo = new Demo();
byte value = 10;
demo.show(value);byte
│
▼
short
│
▼
int
│
▼
long
│
▼
float
│
▼
doubleAmbiguous Method Calls
class Demo {
void show(
int a,
double b
) {
}
void show(
double a,
int b
) {
}
}
Demo demo = new Demo();
demo.show(10, 10);
// Compilation errorBoth overloaded methods are equally valid after type promotion, so the compiler cannot determine which method should be called.
Runtime Polymorphism
Runtime polymorphism occurs when the method implementation is selected while the program is running based on the actual object type.
Animal animal =
new Dog();
animal.sound();REFERENCE TYPE
Animal
ACTUAL OBJECT TYPE
Dog
METHOD CALL
animal.sound()
METHOD EXECUTED
Dog.sound()Method Overriding
Method overriding allows a child class to provide its own implementation of an inherited instance method. It is the foundation of runtime polymorphism.
class Animal {
void sound() {
System.out.println(
"Animal sound"
);
}
}
class Dog extends Animal {
@Override
void sound() {
System.out.println(
"Dog barks"
);
}
}Runtime Polymorphism Example
public class Animal {
public void sound() {
System.out.println(
"Animal makes a sound"
);
}
}public class Dog
extends Animal {
@Override
public void sound() {
System.out.println(
"Dog barks"
);
}
}public class Cat
extends Animal {
@Override
public void sound() {
System.out.println(
"Cat meows"
);
}
}public class Main {
public static void main(
String[] args
) {
Animal animal;
animal = new Dog();
animal.sound();
animal = new Cat();
animal.sound();
}
}Dog barks
Cat meowsReference Type vs Object Type
Animal animal =
new Dog();Animal animal = new Dog();
│ │
▼ ▼
Reference Type Object Type
COMPILE-TIME CHECKING
Uses Reference Type
OVERRIDDEN METHOD EXECUTION
Uses Actual Object Type- The reference type controls which members can be accessed.
- The actual object type controls which overridden instance method executes.
- These two rules are essential for understanding runtime polymorphism.
Dynamic Method Dispatch
Dynamic method dispatch is the mechanism by which Java selects the overridden instance method to execute based on the actual object type at runtime.
Animal animal;
animal = new Dog();
animal.sound();
animal = new Cat();
animal.sound();animal.sound()
│
▼
Check Actual Object
│
├── Dog Object
│
└── Execute Dog.sound()
animal.sound()
│
▼
Check Actual Object
│
├── Cat Object
│
└── Execute Cat.sound()How Runtime Polymorphism Works
Animal animal = new Dog();
│
▼
Compiler Checks
Does Animal declare or inherit sound()?
│
▼
YES
Code Compiles
│
▼
Program Runs
│
▼
Actual Object is Dog
│
▼
Dog overrides sound()?
│
▼
YES
│
▼
Execute Dog.sound()Upcasting and Polymorphism
Upcasting stores a child object in a parent-type reference. This is the most common way to achieve runtime polymorphism.
Dog dog = new Dog();
Animal animal = dog;Animal animal =
new Dog();- One parent type can represent many child types.
- Methods can accept general parent types.
- Collections can store different child implementations.
- Runtime method dispatch becomes possible.
- Code becomes less dependent on concrete classes.
Parent Reference Limitations
class Dog extends Animal {
@Override
void sound() {
System.out.println(
"Dog barks"
);
}
void fetch() {
System.out.println(
"Dog fetches"
);
}
}Animal animal =
new Dog();
animal.sound();
// Not allowed
animal.fetch();The actual object is a Dog, but the reference type is Animal. Only members available through Animal can be accessed directly.
Accessing Child-Specific Members
Dog dog = new Dog();
dog.sound();
dog.fetch();Animal animal =
new Dog();
Dog dog =
(Dog) animal;
dog.fetch();Downcasting
Downcasting converts a parent-type reference to a more specific child-type reference. It requires an explicit cast.
Animal animal =
new Dog();
Dog dog =
(Dog) animal;Animal animal =
new Cat();
Dog dog =
(Dog) animal;
// ClassCastExceptioninstanceof Operator
if (animal instanceof Dog) {
Dog dog =
(Dog) animal;
dog.fetch();
}The instanceof operator checks whether an object is compatible with a particular type before casting.
Pattern Matching with instanceof
if (animal instanceof Dog dog) {
dog.fetch();
}Pattern matching combines the type check and creation of the correctly typed variable.
Polymorphic Arrays
Animal[] animals = {
new Dog(),
new Cat(),
new Bird()
};for (Animal animal : animals) {
animal.sound();
}Dog barks
Cat meows
Bird chirpsPolymorphic Method Parameters
public static void makeSound(
Animal animal
) {
animal.sound();
}makeSound(
new Dog()
);
makeSound(
new Cat()
);
makeSound(
new Bird()
);- One method works with many object types.
- No separate method is required for each child class.
- New child classes can often work without changing the method.
- The correct overridden behavior is selected automatically.
Polymorphic Return Types
static Animal createAnimal(
String type
) {
if (type.equals("dog")) {
return new Dog();
}
return new Cat();
}Animal animal =
createAnimal("dog");
animal.sound();Covariant Return Types
An overriding method can return a more specific reference type than the parent method when the return types are related through inheritance.
class AnimalFactory {
Animal create() {
return new Animal();
}
}
class DogFactory
extends AnimalFactory {
@Override
Dog create() {
return new Dog();
}
}Polymorphism with Abstract Classes
abstract class Shape {
abstract double area();
void displayArea() {
System.out.println(
"Area: " + 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[] shapes = {
new Circle(5),
new Rectangle(10, 5)
};
for (Shape shape : shapes) {
shape.displayArea();
}Polymorphism with Interfaces
interface Payment {
void pay(double amount);
}class CreditCardPayment
implements Payment {
@Override
public void pay(double amount) {
System.out.println(
"Paid ₹"
+ amount
+ " using Credit Card"
);
}
}class UPIPayment
implements Payment {
@Override
public void pay(double amount) {
System.out.println(
"Paid ₹"
+ amount
+ " using UPI"
);
}
}Payment payment;
payment =
new CreditCardPayment();
payment.pay(5000);
payment =
new UPIPayment();
payment.pay(2500);Multiple Implementations
PAYMENT
pay()
/ | \
/ | \
▼ ▼ ▼
CreditCard UPI Wallet
│ │ │
▼ ▼ ▼
Different Different Different
Logic Logic LogicThe calling code can depend on Payment instead of depending directly on CreditCardPayment, UPIPayment, or WalletPayment.
Polymorphism and Dependency Injection
class OrderService {
private final Payment payment;
OrderService(Payment payment) {
this.payment = payment;
}
void checkout(double amount) {
payment.pay(amount);
}
}OrderService cardOrder =
new OrderService(
new CreditCardPayment()
);
OrderService upiOrder =
new OrderService(
new UPIPayment()
);- OrderService depends on Payment rather than a specific payment class.
- The implementation can be changed without modifying OrderService.
- New payment methods can be added more easily.
- Testing becomes easier because test implementations can be injected.
- This principle is widely used in frameworks such as Spring.
Static Methods and Polymorphism
Static methods do not participate in runtime polymorphism. They belong to classes and are resolved using the reference type.
class Parent {
static void display() {
System.out.println(
"Parent"
);
}
}
class Child extends Parent {
static void display() {
System.out.println(
"Child"
);
}
}Parent reference =
new Child();
reference.display();ParentFields and Polymorphism
Fields are not polymorphic. Field access is determined by the reference type.
class Parent {
String name = "Parent";
}
class Child extends Parent {
String name = "Child";
}Parent reference =
new Child();
System.out.println(
reference.name
);ParentPrivate Methods
Private methods are not inherited in a way that allows overriding. A child method with the same name is a separate method.
class Parent {
private void display() {
System.out.println(
"Parent"
);
}
}
class Child extends Parent {
void display() {
System.out.println(
"Child"
);
}
}Final Methods
class Parent {
final void display() {
System.out.println(
"Fixed behavior"
);
}
}A final method cannot be overridden, so its behavior cannot participate in runtime polymorphism through overriding.
Method Overloading vs Method Overriding
METHOD OVERLOADING
Same method name
Different parameter list
Usually same class
Compile-time decision
Inheritance not required
Static methods can be overloaded
METHOD OVERRIDING
Same method signature
Parent-child relationship
Runtime decision
Inheritance required
Static methods are not overriddenEarly Binding vs Late Binding
EARLY BINDING
Decision made at compile time
Used with:
Method overloading
Static methods
Private methods
Final methods
LATE BINDING
Decision made at runtime
Used with:
Overridden instance methods
Dynamic method dispatchCompile-Time vs Runtime Polymorphism
COMPILE-TIME POLYMORPHISM
Achieved through method overloading
Decision made by compiler
Based on method signature
Also called static polymorphism
Generally faster method selection
RUNTIME POLYMORPHISM
Achieved through method overriding
Decision made while program runs
Based on actual object type
Also called dynamic polymorphism
Supports flexible object-oriented designPayment Example
public interface Payment {
boolean pay(double amount);
}public class CreditCardPayment
implements Payment {
@Override
public boolean pay(double amount) {
if (amount <= 0) {
return false;
}
System.out.println(
"Credit card payment: ₹"
+ amount
);
return true;
}
}public class UPIPayment
implements Payment {
@Override
public boolean pay(double amount) {
if (amount <= 0) {
return false;
}
System.out.println(
"UPI payment: ₹"
+ amount
);
return true;
}
}public class CheckoutService {
private final Payment payment;
public CheckoutService(
Payment payment
) {
this.payment = payment;
}
public void checkout(
double amount
) {
if (payment.pay(amount)) {
System.out.println(
"Order completed"
);
}
}
}Employee Example
public abstract class Employee {
private final String name;
public Employee(String name) {
this.name = name;
}
public String getName() {
return name;
}
public abstract double
calculateSalary();
}public class FullTimeEmployee
extends Employee {
private final double
monthlySalary;
public FullTimeEmployee(
String name,
double monthlySalary
) {
super(name);
this.monthlySalary =
monthlySalary;
}
@Override
public double calculateSalary() {
return monthlySalary;
}
}public class ContractEmployee
extends Employee {
private final double hourlyRate;
private final int hoursWorked;
public ContractEmployee(
String name,
double hourlyRate,
int hoursWorked
) {
super(name);
this.hourlyRate = hourlyRate;
this.hoursWorked =
hoursWorked;
}
@Override
public double calculateSalary() {
return hourlyRate
* hoursWorked;
}
}Employee[] employees = {
new FullTimeEmployee(
"Aarav",
60000
),
new ContractEmployee(
"Diya",
500,
80
)
};
for (Employee employee : employees) {
System.out.println(
employee.getName()
+ ": ₹"
+ employee.calculateSalary()
);
}Notification Example
public interface NotificationService {
void send(
String recipient,
String message
);
}public class EmailNotification
implements NotificationService {
@Override
public void send(
String recipient,
String message
) {
System.out.println(
"Email sent to "
+ recipient
);
}
}public class SMSNotification
implements NotificationService {
@Override
public void send(
String recipient,
String message
) {
System.out.println(
"SMS sent to "
+ recipient
);
}
}public class NotificationManager {
private final NotificationService
notificationService;
public NotificationManager(
NotificationService
notificationService
) {
this.notificationService =
notificationService;
}
public void notifyUser(
String recipient,
String message
) {
notificationService.send(
recipient,
message
);
}
}Shape Example
public abstract class Shape {
public abstract double area();
}public class Circle
extends Shape {
private final double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double area() {
return Math.PI
* radius
* radius;
}
}public class Rectangle
extends Shape {
private final double width;
private final double height;
public Rectangle(
double width,
double height
) {
this.width = width;
this.height = height;
}
@Override
public double area() {
return width * height;
}
}public static double totalArea(
Shape[] shapes
) {
double total = 0;
for (Shape shape : shapes) {
total += shape.area();
}
return total;
}Bank Account Example
public abstract class BankAccount {
private double balance;
public BankAccount(
double balance
) {
this.balance = balance;
}
public double getBalance() {
return balance;
}
public abstract double
calculateInterest();
}public class SavingsAccount
extends BankAccount {
public SavingsAccount(
double balance
) {
super(balance);
}
@Override
public double calculateInterest() {
return getBalance() * 0.04;
}
}public class FixedDepositAccount
extends BankAccount {
public FixedDepositAccount(
double balance
) {
super(balance);
}
@Override
public double calculateInterest() {
return getBalance() * 0.07;
}
}Common Polymorphism Errors
POLYMORPHISM ERRORS
├── Confusing Overloading and Overriding
├── Changing Only Return Type for Overloading
├── Creating Ambiguous Overloaded Methods
├── Ignoring Automatic Type Promotion
├── Assuming Overloading Happens at Runtime
├── Using Wrong Parameter Types
├── Forgetting @Override
├── Changing Method Signature Accidentally
├── Reducing Visibility While Overriding
├── Trying to Override final Methods
├── Trying to Override Private Methods
├── Treating Static Methods as Polymorphic
├── Confusing Static Hiding with Overriding
├── Treating Fields as Polymorphic
├── Confusing Reference Type with Object Type
├── Calling Child-Specific Methods Through Parent Reference
├── Performing Unsafe Downcasting
├── Ignoring ClassCastException
├── Using instanceof Everywhere
├── Excessive Type Checking
├── Using Downcasting Instead of Polymorphism
├── Depending on Concrete Classes
├── Creating Separate Logic for Every Child Type
├── Using if-else Chains Instead of Overriding
├── Using switch Statements for Object Types
├── Breaking Parent Method Contracts
├── Returning Incompatible Types
├── Misusing Covariant Return Types
├── Forgetting Runtime Dispatch Applies to Instance Methods
├── Assuming Constructors are Polymorphic
├── Calling Overridable Methods from Constructors
├── Creating Overly Complex Hierarchies
├── Using Inheritance Without IS-A Relationship
├── Ignoring Interfaces
├── Ignoring Abstract Classes
├── Overusing Method Overloading
├── Creating Too Many Similar Overloads
├── Using null with Ambiguous Overloads
├── Forgetting Boxing and Unboxing Effects
├── Forgetting Varargs Can Affect Overload Resolution
├── Assuming Parent Reference Changes the Object
├── Assuming Upcasting Creates a New Object
├── Assuming Downcasting Changes the Object
├── Mixing Field Hiding with Method Overriding
├── Depending on Reference Type for Overridden Methods
├── Depending on Object Type for Fields
├── Tight Coupling to Implementations
├── Violating Substitution Expectations
├── Adding Child Behavior That Breaks Parent Contracts
├── Poor Interface Design
├── God Interfaces
├── Excessive Downcasting
└── Missing the Main Purpose of PolymorphismBest Practices
- Program to abstractions instead of concrete implementations.
- Use parent classes, abstract classes, and interfaces as reference types.
- Use runtime polymorphism to remove unnecessary type-based conditions.
- Use @Override whenever overriding a method.
- Keep overridden behavior compatible with parent expectations.
- Do not reduce method visibility when overriding.
- Use meaningful method overloading.
- Avoid excessive overloads that make APIs confusing.
- Avoid ambiguous overloaded method signatures.
- Do not rely only on return types for overloading.
- Understand automatic type promotion.
- Understand boxing, unboxing, and varargs when designing overloads.
- Use upcasting naturally.
- Avoid unnecessary downcasting.
- Use instanceof only when type-specific behavior is genuinely required.
- Prefer polymorphic method calls over repeated type checks.
- Use interfaces when unrelated classes share a common capability.
- Use abstract classes when implementations share state and behavior.
- Keep interfaces focused.
- Keep abstractions stable.
- Inject implementations instead of creating them inside dependent classes.
- Use constructor injection for required dependencies.
- Use polymorphic arrays and collections for related object types.
- Accept general types in method parameters.
- Return abstractions when callers do not need concrete implementation details.
- Use covariant return types when a more specific result improves the API.
- Remember that static methods are not runtime polymorphic.
- Remember that fields are not runtime polymorphic.
- Remember that private methods are not overridden.
- Remember that final methods cannot be overridden.
- Keep inheritance hierarchies shallow.
- Use genuine IS-A relationships.
- Prefer composition when behavior should be replaceable dynamically.
- Design child classes as valid substitutes for parent classes.
- Do not break parent contracts in overridden methods.
- Avoid throwing unsupported-operation exceptions for expected parent behavior.
- Use polymorphism to make systems extensible.
- Add new implementations without modifying stable calling code when possible.
- Test implementations through their abstraction.
- Use test doubles when appropriate.
- Keep business logic independent of implementation details.
- Avoid large if-else chains based on object type.
- Avoid switch statements that repeatedly inspect implementation types.
- Let objects perform their own specialized behavior.
- Use descriptive abstraction names.
- Keep methods cohesive.
- Document important contracts.
- Test runtime dispatch behavior.
- Understand reference type and object type separately.
- Use polymorphism as a design principle, not merely as syntax.
Common Misconceptions
- Polymorphism does not mean only method overriding.
- Java supports both compile-time and runtime polymorphism.
- Method overloading is resolved at compile time.
- Method overriding is resolved at runtime for eligible instance methods.
- Return type alone cannot overload a method.
- Overloaded methods do not require inheritance.
- Overridden methods require an inheritance or implementation relationship.
- Static methods are not runtime polymorphic.
- Fields are not runtime polymorphic.
- Private methods are not overridden.
- Final methods cannot be overridden.
- Constructors are not overridden.
- A parent reference can refer to a child object.
- A child reference cannot directly refer to a general parent object.
- Upcasting does not create a new object.
- Downcasting does not change the actual object.
- The reference type controls accessible members.
- The actual object type controls overridden method execution.
- A parent reference cannot directly call child-specific methods.
- Downcasting can fail at runtime.
- instanceof should not replace good polymorphic design.
- Runtime polymorphism requires eligible overridden instance methods.
- Method overloading is not dynamic method dispatch.
- The same object can be referenced through different compatible types.
- Interfaces are major tools for polymorphic design.
- Abstract classes also support polymorphism.
- Polymorphism reduces dependency on concrete classes.
- Polymorphism does not automatically guarantee good design.
- Too much downcasting often indicates a weak abstraction.
- Repeated type checking may indicate missing polymorphic behavior.
Practice Exercises
- Create a Calculator class.
- Overload add() for two integers.
- Overload add() for three integers.
- Overload add() for two doubles.
- Test all methods.
- Create an Animal parent class.
- Create Dog, Cat, and Bird child classes.
- Override sound().
- Store all objects in an Animal array.
- Call sound() using a loop.
- Create a Payment interface.
- Create CreditCardPayment.
- Create UPIPayment.
- Create WalletPayment.
- Process all payments through Payment references.
- Create an abstract Shape class.
- Create Circle and Rectangle.
- Override area().
- Store shapes in an array.
- Calculate total area polymorphically.
- Create an abstract Employee class.
- Create FullTimeEmployee.
- Create ContractEmployee.
- Override calculateSalary().
- Process payroll through Employee references.
- Create a NotificationService interface.
- Create EmailNotification.
- Create SMSNotification.
- Create PushNotification.
- Inject implementations into a NotificationManager.
- Create Animal and Dog classes.
- Store a Dog in an Animal reference.
- Check the type using instanceof.
- Use pattern matching.
- Call a Dog-specific method safely.
- Create code that uses if-else based on object type.
- Identify the changing behavior.
- Move behavior into overridden methods.
- Use a parent type or interface.
- Compare both designs.
Common Interview Questions
What is polymorphism in Java?
Polymorphism allows one method name, reference type, abstract class, or interface to represent multiple forms of behavior.
What are the types of polymorphism in Java?
The two main types are compile-time polymorphism and runtime polymorphism.
How is compile-time polymorphism achieved?
It is generally achieved through method overloading.
How is runtime polymorphism achieved?
It is achieved through method overriding and dynamic method dispatch.
What is method overloading?
Method overloading allows methods with the same name to have different parameter lists.
Can return type alone overload a method?
No.
What is method overriding?
Method overriding occurs when a child class provides its own implementation of an inherited instance method.
What is dynamic method dispatch?
It is the runtime mechanism that selects an overridden method based on the actual object type.
What is upcasting?
Upcasting stores a child object in a parent-type reference.
What controls accessible members?
The reference type controls which members can be accessed at compile time.
What controls overridden method execution?
The actual object type controls which overridden instance method executes.
Are static methods polymorphic?
Static methods do not participate in runtime polymorphism. They are hidden rather than overridden.
Are fields polymorphic?
No. Field access is based on the reference type.
What is a covariant return type?
It allows an overriding method to return a more specific reference type than the parent method.
What is the difference between early and late binding?
Early binding resolves a call at compile time, while late binding resolves an overridden instance method at runtime.
Frequently Asked Questions
Can one parent reference point to different child objects?
Yes. This is a central use of runtime polymorphism.
Can a parent reference call child-specific methods?
Not directly. The member must be available through the reference type, or safe downcasting must be used.
Does upcasting create a new object?
No. It creates another compatible reference to the same object.
Does downcasting change the object?
No. It only changes the reference type used to access the existing object.
Should I use instanceof frequently?
Usually no. Frequent type checks may indicate that behavior should be handled polymorphically.
Are interfaces better for polymorphism?
Interfaces are excellent for capability-based polymorphism and loose coupling, while abstract classes are useful when implementations also share state or common behavior.
What should I learn after Polymorphism?
The next lesson covers Abstraction in Java.
Key Takeaways
- Polymorphism means many forms.
- Polymorphism is a fundamental Object-Oriented Programming principle.
- The same method call can produce different behavior.
- One parent reference can represent different child objects.
- Java supports compile-time and runtime polymorphism.
- Compile-time polymorphism is generally achieved through method overloading.
- Runtime polymorphism is achieved through method overriding.
- Method overloading uses the same method name with different parameter lists.
- Overloading can differ by parameter count.
- Overloading can differ by parameter types.
- Overloading can differ by parameter order.
- Return type alone cannot overload a method.
- Static methods can be overloaded.
- Constructors can be overloaded.
- Automatic type promotion can affect overload selection.
- Ambiguous overloaded calls cause compilation errors.
- Method overriding requires an inheritance or implementation relationship.
- The @Override annotation should be used.
- Dynamic method dispatch selects overridden methods at runtime.
- The reference type controls accessible members.
- The actual object type controls overridden method execution.
- Upcasting enables runtime polymorphism.
- Upcasting is automatic and safe.
- A parent reference cannot directly access child-specific members.
- Downcasting requires an explicit cast.
- Invalid downcasting can cause ClassCastException.
- instanceof can check type compatibility.
- Pattern matching simplifies instanceof usage.
- Polymorphic arrays can store different child objects.
- Polymorphic method parameters allow one method to process many implementations.
- Polymorphic return types allow methods to return different implementations.
- Covariant return types allow more specific return types in overriding methods.
- Abstract classes support runtime polymorphism.
- Interfaces strongly support flexible polymorphic design.
- One interface can have many implementations.
- Dependency injection relies heavily on polymorphism.
- Static methods do not provide runtime polymorphism.
- Fields are not polymorphic.
- Private methods are not overridden.
- Final methods cannot be overridden.
- Overloading is generally compile-time behavior.
- Overriding provides runtime behavior.
- Early binding occurs at compile time.
- Late binding occurs at runtime.
- Polymorphism reduces dependency on concrete classes.
- Polymorphism supports loose coupling.
- Polymorphism improves extensibility.
- New implementations can often be added without changing calling code.
- Repeated type checks can often be replaced with polymorphism.
- Excessive downcasting may indicate a poor abstraction.
- Good polymorphic design depends on stable abstractions.
- Child implementations should preserve parent contracts.
- Polymorphism is widely used in real applications and frameworks.
Summary
Polymorphism means many forms. It allows one method name, parent type, abstract class, or interface to represent different implementations and behaviors.
Java supports compile-time polymorphism and runtime polymorphism. Compile-time polymorphism is generally achieved through method overloading, while runtime polymorphism is achieved through method overriding and dynamic method dispatch.
Method overloading allows multiple methods to use the same name with different parameter lists. The compiler determines which overloaded method should execute by examining the method arguments.
Runtime polymorphism allows a parent reference to refer to different child objects. The reference type controls which members are accessible, while the actual object type determines which overridden instance method executes.
Upcasting is central to runtime polymorphism because it allows child objects to be handled through general parent, abstract-class, or interface references. Downcasting should be used only when child-specific behavior is genuinely required.
Abstract classes and interfaces allow multiple implementations to be processed through common abstractions. This reduces coupling and makes applications easier to extend, test, and maintain.
Static methods and fields do not participate in runtime polymorphism. Static method calls and field access depend on the reference or class type, while overridden instance methods depend on the actual object type.
Good polymorphic design depends on meaningful abstractions, compatible implementations, limited downcasting, and code that delegates specialized behavior to objects instead of repeatedly checking their concrete types.
- You understand what polymorphism is.
- You understand why polymorphism is useful.
- You know the types of polymorphism.
- You understand compile-time polymorphism.
- You can create overloaded methods.
- You know method overloading rules.
- You understand valid and invalid overloading.
- You understand automatic type promotion.
- You understand ambiguous method calls.
- You understand runtime polymorphism.
- You understand method overriding.
- You understand reference type and object type.
- You understand dynamic method dispatch.
- You understand how runtime method selection works.
- You can use upcasting.
- You understand parent reference limitations.
- You can safely use downcasting.
- You can use instanceof.
- You understand pattern matching with instanceof.
- You can create polymorphic arrays.
- You can create polymorphic method parameters.
- You understand polymorphic return types.
- You understand covariant return types.
- You understand polymorphism with abstract classes.
- You understand polymorphism with interfaces.
- You understand multiple implementations.
- You understand polymorphism and dependency injection.
- You understand why static methods are not runtime polymorphic.
- You understand why fields are not polymorphic.
- You know the difference between overloading and overriding.
- You understand early and late binding.
- You can design flexible polymorphic systems.
- You can identify common polymorphism errors.
- You know polymorphism best practices.
- You are ready to learn Abstraction in Java.