Java OOP Interview Questions: Polymorphism, Abstraction & More
- Encapsulation isn't about getters and setters — it's about protecting invariants so no external code can put your object into an illegal state.
- Abstraction defines the contract (what must be done); polymorphism executes it (which version runs at runtime). They're partners, not synonyms.
- Choose abstract class when subtypes share instance state or a genuine is-a relationship. Choose interface for capabilities that apply across unrelated class hierarchies.
Think of a TV remote. You press 'Volume Up' and the TV gets louder — you don't care how the TV processes that signal internally. OOP works the same way: you interact with objects through simple buttons (methods), while the complex wiring stays hidden inside. Polymorphism means that same 'Volume Up' button works on a Samsung AND a Sony. Abstraction is why you never need to open the TV to change the channel. That's Java OOP in one analogy.
If you're interviewing for a Java role — junior, mid, or senior — OOP questions are guaranteed to show up. Not because interviewers love theory, but because the way you answer reveals how you actually design software. A candidate who can recite four pillars from memory is forgettable. A candidate who explains WHY encapsulation prevents bugs in a multi-team codebase gets the offer.
The problem is that most resources teach OOP as a list of definitions. That leaves you able to parrot answers but unable to handle the natural follow-up: 'Can you give me a real-world example?' or 'How does that differ from an abstract class?' Those follow-ups are where interviews are actually won or lost.
After working through this article, you'll be able to explain every major OOP concept with a concrete analogy, write runnable code that demonstrates each idea, spot the three classic mistakes candidates make, and handle the tricky follow-up questions interviewers use to separate the memorisers from the thinkers.
The Four Pillars — What They Are and Why Each One Exists
Every Java OOP interview starts here. The four pillars are Encapsulation, Abstraction, Inheritance, and Polymorphism. But interviewers don't want a dictionary. They want to know you understand the problem each pillar solves.
Encapsulation solves the 'who changed my data?' problem. By bundling data with the methods that operate on it and hiding the internals, you prevent other parts of the system from putting an object into an invalid state. Think of a bank account — you never want external code to set the balance directly to a negative number.
Abstraction solves the 'I don't need to know how' problem. You expose only what's necessary and hide everything else. This is why you can call without understanding TimSort.list.sort()
Inheritance solves the 'don't repeat yourself' problem. Common behaviour lives in a parent class; child classes inherit it and specialise where needed.
Polymorphism solves the 'treat different things uniformly' problem. One interface, many implementations. This is what makes your code extensible without modification — the Open/Closed Principle in action.
package io.thecodeforge.oop; /** * Production-grade Encapsulation Example. * We protect the 'balance' invariant from external corruption. */ public class BankAccount { private double balance; private final String accountId; public BankAccount(String accountId, double initialDeposit) { this.accountId = accountId; validateAndSetBalance(initialDeposit); } public void deposit(double amount) { if (amount <= 0) { throw new IllegalArgumentException("Deposit must be positive"); } this.balance += amount; } public void withdraw(double amount) { if (amount > balance) { throw new IllegalStateException("Insufficient funds for account: " + accountId); } this.balance -= amount; } public double getBalance() { return balance; } private void validateAndSetBalance(double amount) { if (amount < 0) throw new IllegalArgumentException("Initial balance cannot be negative"); this.balance = amount; } }
Polymorphism vs Abstraction — The Question That Trips Everyone Up
These two are the most commonly confused pillars, and interviewers exploit that confusion heavily. Here's the clean separation: Abstraction is about DESIGN — hiding complexity behind a simple interface. Polymorphism is about BEHAVIOUR — the same call producing different results depending on the actual object at runtime.
Abstraction is implemented in Java via abstract classes and interfaces. You define WHAT something must do without specifying HOW. Polymorphism is what happens at runtime when Java resolves which overridden method to actually call.
A classic follow-up: 'What's the difference between method overloading and method overriding?' Overloading is compile-time polymorphism — same method name, different parameters, resolved by the compiler. Overriding is runtime polymorphism — same signature in parent and child, resolved by the JVM based on the actual object type.
package io.thecodeforge.oop; import java.util.List; // ABSTRACTION: The 'What' interface Notifier { void send(String message); } // POLYMORPHISM: The 'How' class EmailNotifier implements Notifier { @Override public void send(String msg) { System.out.println("Emailing: " + msg); } } class SlackNotifier implements Notifier { @Override public void send(String msg) { System.out.println("Slacking: " + msg); } } public class NotificationService { public void broadcast(List<Notifier> recipients, String message) { // Polymorphic call: The service doesn't care about concrete types recipients.forEach(n -> n.send(message)); } }
Abstract Classes vs Interfaces — When to Use Which
This is arguably the most asked Java OOP question at mid-level interviews. Both enforce a contract. Both support polymorphism. But they're not interchangeable, and using the wrong one reveals a gap in design thinking.
Use an abstract class when you have a true 'is-a' relationship AND shared state or behaviour to inherit. Example: a Vehicle abstract class that stores fuelLevel and has a concrete method that all vehicles share. Child classes extend this and implement their own refuel() method.accelerate()
Use an interface when you're defining a capability that could apply to completely unrelated classes. Serializable, Comparable, and Runnable are capabilities, not identities. A Dog and a BankTransaction can both be Serializable — that doesn't mean they share a parent.
Since Java 8, interfaces can have default and static methods, which blurs the line slightly. The practical rule: if you need instance state (fields) in the shared contract, you need an abstract class. Interfaces can't hold instance state.
package io.thecodeforge.oop; // Abstract Class: Shared state (identity) abstract class BaseVehicle { protected int fuelLevel; public void refuel(int amount) { this.fuelLevel += amount; } public abstract void drive(); } // Interface: Shared capability interface GPS { String getCoordinates(); } class SmartCar extends BaseVehicle implements GPS { @Override public void drive() { fuelLevel -= 5; } @Override public String getCoordinates() { return "51.5074 N, 0.1278 W"; } }
Inheritance Pitfalls and the Liskov Substitution Principle
Inheritance looks clean on paper but is the most misused OOP feature in real codebases. The classic mistake: using inheritance for code reuse when there's no genuine 'is-a' relationship. Stack extending Vector in Java's own standard library is the canonical example of this done badly — a Stack is NOT a Vector, but Java's designers used inheritance for convenience, which meant Stack accidentally exposed methods like add(int index, Object element) that make no logical sense for a stack.
The Liskov Substitution Principle (LSP) is the interview gold standard here. It says: if you replace a parent with any of its subtypes, the program should still behave correctly. A Square extending Rectangle violates LSP — if you set width on a Square it must also change the height, which breaks any code that expects to set width and height independently.
When LSP is in danger, favour composition over inheritance. Instead of Square extending Rectangle, give Square a Dimensions object internally.
package io.thecodeforge.oop; /** * Demonstrating LSP: Any subtype of Payment must be substitutable * without breaking the 'process' logic. */ public abstract class Payment { public abstract void process(double amount); } class CreditCardPayment extends Payment { @Override public void process(double amount) { System.out.println("Charging credit card: $" + amount); } } class RefundablePayment extends Payment { @Override public void process(double amount) { System.out.println("Processing refundable payment: $" + amount); } public void refund(double amount) { System.out.println("Refunding: $" + amount); } }
| Aspect | Abstract Class | Interface |
|---|---|---|
| Can hold instance state (fields) | Yes — instance fields allowed | No — only static final constants |
| Constructor | Yes — can define constructors | No — interfaces have no constructors |
| Inheritance limit | Single parent class only | A class can implement unlimited interfaces |
| Method types allowed | Abstract + concrete + static | Abstract + default + static (Java 8+) |
| Access modifiers on methods | Any modifier (private, protected, public) | Public by default (private since Java 9) |
| Best used when | Shared state + is-a relationship exists | Shared capability across unrelated classes |
| Real Java example | AbstractList in java.util | Comparable, Runnable, Serializable |
🎯 Key Takeaways
- Encapsulation isn't about getters and setters — it's about protecting invariants so no external code can put your object into an illegal state.
- Abstraction defines the contract (what must be done); polymorphism executes it (which version runs at runtime). They're partners, not synonyms.
- Choose abstract class when subtypes share instance state or a genuine is-a relationship. Choose interface for capabilities that apply across unrelated class hierarchies.
- Liskov Substitution Principle is your inheritance sanity check: if you can't swap a subtype in wherever the parent is expected without breaking anything, you've got bad inheritance — fix it with composition.
⚠ Common Mistakes to Avoid
Interview Questions on This Topic
- QExplain the 'Diamond Problem' in Java. Why does it not occur with interfaces even with default methods, but would occur with multiple class inheritance?
- QWhy is it said that 'Composition is better than Inheritance'? Provide a scenario where favoriting inheritance would lead to a rigid architecture.
- QIf a parent class constructor calls an overridden method, what is the risk? Walk through the object initialization order and explain the potential for NullPointerException.
Frequently Asked Questions
What is 'Shadowing' vs 'Overriding' in Java?
Shadowing occurs when a variable in a subclass has the same name as a variable in the parent class; this is resolved based on the reference type. Overriding applies to methods and is resolved based on the actual object type at runtime. Seniors should note that shadowing variables is generally considered bad practice as it violates encapsulation and clarity.
Why does Java not support multiple inheritance of classes?
Primarily to avoid the 'Diamond Problem,' where a subclass might inherit conflicting implementations of the same method from two different parent classes. Java allows multiple inheritance of type via interfaces, resolving method conflicts through specific rules for default methods introduced in Java 8.
What is the difference between method overloading and method overriding in Java?
Overloading is compile-time polymorphism: multiple methods in the same class share a name but differ in parameter type or count, and the compiler decides which to call. Overriding is runtime polymorphism: a subclass provides its own implementation of a parent method with the identical signature, and the JVM decides which to call based on the actual object type — not the reference type.
Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.