Mastering Java Beyond Classes — Part 7: Polymorphism in Java

Polymorphism is one of the core pillars of Object-Oriented Programming and also one of the most misunderstood.

One interface, multiple implementations?

…but fail when asked:

  • What actually happens at runtime?
  • Why does method overloading behave differently?
  • What does the JVM really call?

This article fixes that.

What Is Polymorphism?

Polymorphism uses the same method name, different behaviour. There are two types in Java:

Compile-Time Polymorphism (Method Overloading)

Same method name, different parameters.

class Calculator {

int add(int a, int b) {
return a + b;
}

double add(double a, double b) {
return a + b;
}
}

public class TestOverloading {
public static void main(String[] args) {
Calculator calc = new Calculator();

System.out.println(calc.add(2, 3));
System.out.println(calc.add(2.5, 3.5));
}
}

Output

5
6.0

Runtime Polymorphism (Method Overriding)

Now we’re talking real polymorphism.

class Animal {
void sound() {
System.out.println("Animal makes sound");
}
}

class Dog extends Animal {
@Override
void sound() {
System.out.println("Dog barks");
}
}

public class TestPolymorphism {
public static void main(String[] args) {
Animal obj = new Dog();
obj.sound();
}
}

Output

Dog barks

What’s Really happening?

  • Reference type — Animal
  • Object type — Dog
  • JVM decides the method at runtime

This is called: Dynamic Method Dispatch

Method calls depend on the OBJECT type
Variable access depends on the REFERENCE type

Look at the following example. What is the Output?

class Parent {
int value = 10;

void show() {
System.out.println("Parent show()");
}
}

class Child extends Parent {
int value = 20;

@Override
void show() {
System.out.println("Child show()");
}
}

public class Test {
public static void main(String[] args) {
Parent obj = new Child();

System.out.println(obj.value);
obj.show();
}
}

Output

10
Child show()

Explanation

  • obj.value — uses Parent reference
  • obj.show() — uses Child object

Upcasting and Downcasting

Upcasting (Safe)

Animal a = new Dog();
  • Done automatically
  • Enables polymorphism

Downcasting (Risky)

Dog d = (Dog) a;
  • Requires explicit cast
  • Can cause a runtime error

Dangerous Example

Animal a = new Animal();
Dog d = (Dog) a; // Runtime error

ClassCastException

Real-World Example

interface Payment {
void pay();
}

class CreditCard implements Payment {
public void pay() {
System.out.println("Paid using Credit Card");
}
}
class PayPal implements Payment {
public void pay() {
System.out.println("Paid using PayPal");
}
}
public class PaymentDemo {
public static void main(String[] args) {
Payment p;
p = new CreditCard();
p.pay();
p = new PayPal();
p.pay();
}
}

Output

Paid using Credit Card
Paid using PayPal

Same interface → different behavior
This is real-world polymorphism

Practice Questions

Question 1

What will be the output?

class A {
void show() {
System.out.println("A");
}
}
class B extends A {
void show() {
System.out.println("B");
}
}
public class Test {
public static void main(String[] args) {
A obj = new B();
obj.show();
}
}

Question 2

What will be the output?

class Test {
void show(int a) {
System.out.println("int");
}
void show(double a) {
System.out.println("double");
}
public static void main(String[] args) {
new Test().show(10);
}
}

Question 3

What will be the output?

class Parent {
static void show() {
System.out.println("Parent");
}
}

class Child extends Parent {
static void show() {
System.out.println("Child");
}
}
public class Test {
public static void main(String[] args) {
Parent obj = new Child();
obj.show();
}
}

Bonus Challenge

What is the output?

class Animal {
void run() {
System.out.println("Animal");
}
}

class Dog extends Animal {
void run() {
System.out.println("Dog");
}
}

public class Test {
public static void main(String[] args) {
Animal a = new Dog();
((Animal)a).run();
}
}

Common Mistakes

  • Thinking overloading = runtime polymorphism
  • Forgetting @Override annotations
  • Confusing variable vs method behavior
  • Downcasting blindly
  • Expecting static methods to behave polymorphically

Best Practices

  • Always use interfaces and polymorphism
  • Prefer runtime polymorphism over conditionals
  • Use @Override to avoid silent bugs
  • Avoid unnecessary casting
  • Write code like:
List<String> list = new ArrayList<>();

Program to interfaces, not implementations.

If you found this article helpful:

  • Follow for more Java tutorials
  • Share your answers in the comments
  • Save this guide for future reference

Conclusion

Polymorphism is far more than an interview topic — it is one of the foundations of scalable software design.

It allows developers to write flexible, reusable, and maintainable code by programming to abstractions rather than concrete implementations.

Throughout this article, we explored:

  • Compile-time polymorphism (method overloading)
  • Runtime polymorphism (method overriding)
  • Dynamic method dispatch
  • Upcasting and downcasting
  • Real-world interface-based polymorphism
  • Common pitfalls and best practices

Final Rule to Remember

Methods are resolved by object type.
Variables are resolved by reference type.

If you understand that rule deeply, you understand the heart of Java polymorphism.

So this is the end of my Mastering Java Beyond Classes series. See you in the next series, where we dive deeper into advanced Java.

Thank you!

Have a great day!


Mastering Java Beyond Classes — Part 7: Polymorphism in Java was originally published in Javarevisited on Medium, where people are continuing the conversation by highlighting and responding to this story.

This post first appeared on Read More