Use interfaces when you care about what an object can do, and use abstract classes when you also need shared state or shared code. That is the simple rule. Polymorphism in Java lets your code talk to many object types through one common shape. Less fuss. Fewer giant if blocks. Happier future you.
TLDR: Polymorphism lets one variable call the right behavior on many different objects. For example, a checkout app can call pay() on CardPayment, PayPalPayment, or WalletPayment without caring which one it got. In one small shop app with 12 payment methods, using an interface removed about 70% of the old payment branching code. Use an interface for a role, and an abstract class for a family with shared guts.
Polymorphism, without the headache
Polymorphism sounds like a monster from a sci-fi movie. It is not. It just means many forms.
In Java, polymorphism lets you write code like this:
Payment payment = new CardPayment();
payment.pay(49.99);
Then later:
Payment payment = new WalletPayment();
payment.pay(49.99);
The calling code stays the same. The behavior changes based on the actual object. That is the neat trick.
Think of a TV remote. You press power. Different TVs may wake up in different ways. You do not care. The button is the contract.
Interfaces: the clean little contracts
An interface says, “Any class that signs this contract must provide these methods.” It focuses on ability.
Example:
interface Payment {
void pay(double amount);
}
class CardPayment implements Payment {
public void pay(double amount) {
System.out.println("Paid by card: " + amount);
}
}
class WalletPayment implements Payment {
public void pay(double amount) {
System.out.println("Paid by wallet: " + amount);
}
}
Now your checkout code can stay small:
void completeOrder(Payment payment) {
payment.pay(49.99);
}
No screaming pile of switch statements. No checking every payment type by hand. Honestly, it feels like magic the first time a messy class drops from 300 lines to 90.
Use an interface when:
- You want to define a role.
- Classes may come from unrelated families.
- You want easy testing with fake objects.
- You expect more implementations later.
- You need a class to support more than one role.
That last point matters. Java allows a class to implement many interfaces.
class SmartWatch implements Payment, Notifier, StepCounter {
public void pay(double amount) {}
public void notifyUser(String message) {}
public int stepsToday() { return 8000; }
}
A smartwatch can pay. It can notify. It can count steps. These are separate skills. Interfaces fit this model well.
Abstract classes: the half-built machines
An abstract class is different. It can define methods. It can also store fields. It can provide shared code. But you cannot create it directly.
It is useful when classes are clearly part of the same family.
abstract class Animal {
protected String name;
Animal(String name) {
this.name = name;
}
void sleep() {
System.out.println(name + " is sleeping.");
}
abstract void makeSound();
}
class Dog extends Animal {
Dog(String name) {
super(name);
}
void makeSound() {
System.out.println("Woof!");
}
}
Here, every animal has a name. Every animal can sleep. But each animal makes sound in its own way.
That is polymorphism again:
Animal animal = new Dog("Biscuit");
animal.makeSound();
The variable type is Animal. The object is Dog. Java calls the dog version. Nice.
Use an abstract class when:
- You need shared fields like
id,name, orcreatedAt. - You need shared helper methods.
- You want a base constructor.
- The child classes are closely related.
- You want to control part of the algorithm.
It drives me crazy that people sometimes use abstract classes just because they sound “more serious.” Then the app grows. One class needs a second base class. Java says no. Expect to waste half a day untangling that knot.
The big difference in plain English
| Feature | Interface | Abstract class |
|---|---|---|
| Main idea | A contract | A shared base |
| Best for | Abilities | Families |
| Multiple use | A class can implement many | A class can extend only one |
| Fields | Constants only | Instance fields allowed |
| Constructors | No constructors | Constructors allowed |
| Shared code | Possible with default methods | Natural fit |
A simple app story
Imagine a food delivery app. It needs drivers, restaurants, customers, orders, payments, and alerts.
Some parts are roles:
PayableTrackableNotifiableReviewable
These should be interfaces. A customer can be notifiable. A driver can be notifiable. A restaurant can be reviewable. The classes are not the same kind of thing. They only share a skill.
Other parts are families:
UserOrderMenuItem
These may work better as abstract classes. A Customer and Driver may both be users. They may share an id, a name, a phone number, and login behavior.
abstract class User {
protected long id;
protected String name;
void login() {
System.out.println(name + " logged in.");
}
}
class Customer extends User implements Notifiable {
public void sendNotification(String text) {
System.out.println("Customer alert: " + text);
}
}
This mix is common. It is also powerful. Use abstract classes for the spine. Use interfaces for the add-on skills.
What about default methods?
Java interfaces can have default methods. This means an interface can include a method body.
interface Notifiable {
void sendNotification(String text);
default void sendWelcomeMessage() {
sendNotification("Welcome!");
}
}
This is handy. But be careful. If your interface starts holding too much behavior, it can become weird. Keep interfaces lean when you can.
A default method is best for light shared behavior. It is not a storage locker. It has no instance fields. If you need state, reach for an abstract class.
Testing becomes less painful
Interfaces make tests simple. Say your service needs to send email.
interface MailSender {
void send(String to, String message);
}
In production, use a real sender. In tests, use a fake one.
class FakeMailSender implements MailSender {
public void send(String to, String message) {
System.out.println("Fake email sent.");
}
}
No real email goes out. No angry test inbox. No waiting 6 seconds for some external mail server to respond. That is a win.
Common mistakes
- Using inheritance for everything. Not every class needs a parent.
- Making huge interfaces. A class should not implement methods it does not need.
- Putting business logic everywhere. Shared code should have one clear home.
- Choosing abstract classes too early. You may block future design options.
- Naming interfaces badly. Use clear names like
Runnable,Payable, orValidator.
A quick decision guide
Ask these questions:
- Is this about a skill? Use an interface.
- Is this about a shared identity? Use an abstract class.
- Do I need multiple roles? Use interfaces.
- Do I need shared fields? Use an abstract class.
- Will unrelated classes use it? Use an interface.
Polymorphism is not there to impress people. It is there to keep code bendy without turning it into soup. Interfaces give you clean contracts. Abstract classes give you shared structure. Use both with care, and your Java apps will be easier to change, test, and understand.