Senior 4 min · March 06, 2026

Java OOP Interview Questions — The $23K @Override Bug

A missing @Override caused a $23K refund bug in Java: non-refundable payments silently did nothing.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • Encapsulation protects invariants, not just data hiding
  • Abstraction defines contracts (what), polymorphism executes them (how)
  • Abstract classes for shared state; interfaces for cross-hierarchy capabilities
  • LSP is your sanity check for inheritance — if you can't substitute, use composition
  • Overloading = compile-time, overriding = runtime — interviewers love this distinction
Plain-English First

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 list.sort() without understanding TimSort.

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.

io/thecodeforge/oop/BankAccount.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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;
    }
}
Output
Class compiled safely. Balance invariant is protected.
Interview Gold:
When asked about encapsulation, mention that it's not just about getters and setters — it's about preserving invariants. A BankAccount with a public balance field has no encapsulation even if it technically compiles. Interviewers love this distinction.
Production Insight
Production systems often fail because a public setter allowed invalid data through.
Encapsulation isn't just academic — it's the difference between a crash at 3 AM and a clean validation error at request time.
Rule: every field mutation must preserve the object's invariant, or it's not encapsulated.
Key Takeaway
Encapsulation ensures no external code can break your object's rules.
Abstraction hides complexity behind a clean contract.
Inheritance and Polymorphism make code reusable and extensible.
Know the problem each pillar solves, not just the definition.

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.

io/thecodeforge/oop/NotificationService.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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));
    }
}
Output
Service ready. Decoupled from specific notification implementations.
The Real Distinction:
Abstraction asks 'what contract must be fulfilled?' — Polymorphism asks 'which fulfillment runs right now?' They work together: abstraction creates the contract, polymorphism executes it. Nail this in an interview and you'll stand out from candidates who treat them as synonyms.
Production Insight
In a real microservice that uses strategy pattern, forgetting to mark the interface method as 'default' can cause compilation errors when a new implementation is added without every client updating.
Polymorphism via interfaces allows you to add new behaviour without modifying existing code — OCP in action.
Misunderstanding the distinction leads to designs where abstraction leaks implementation details.
Key Takeaway
Abstraction defines the contract (what must be done).
Polymorphism executes the right version (which implementation runs).
Overloading happens at compile, overriding at runtime.
When to Use Overloading vs Overriding
IfSame method name, different parameters, same class
UseOverloading — compile-time polymorphism
IfSame method name, same parameters, different class (parent-child)
UseOverriding — runtime polymorphism

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 refuel() method that all vehicles share. Child classes extend this and implement their own accelerate() method.

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.

io/thecodeforge/oop/VehicleRegistry.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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"; }
}
Output
SmartCar inherits identity from BaseVehicle and capability from GPS.
Watch Out:
A very common interview trap: 'From Java 8, interfaces have default methods — so are they just abstract classes now?' No. Interfaces still cannot hold instance state (fields with values per object). That single difference drives the entire design decision. If your shared contract needs to track state per object, you need an abstract class.
Production Insight
Many production codebases misuse interfaces by putting shared state into an interface via default methods that access a singleton map — a hack that breaks thread safety.
Abstract classes are safer when you need shared mutable state; interfaces are cleaner for immutable behaviour contracts.
The rule: if you find yourself adding a default method that relies on a static field, you probably need an abstract class.
Key Takeaway
Abstract class for shared state + 'is-a' relationship.
Interface for shared capability across unrelated classes.
Instance state is the dealbreaker — interfaces can't have it.

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.

io/thecodeforge/oop/PaymentProcessor.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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);
    }
}
Output
Payment hierarchy follows LSP. RefundablePayment adds behavior without breaking Base.
Interview Gold:
If an interviewer asks about inheritance best practices, say 'I try to follow LSP — every subtype should be substitutable for its parent without breaking callers. When I can't guarantee that, it's usually a sign I need composition rather than inheritance.' This one sentence shows you know SOLID principles without being asked directly.
Production Insight
A real payment system failed because a 'RefundablePayment' subclass modified the base 'process' method's preconditions.
One line of code that assumed all payments could be refunded caused a chain of null pointers.
LSP isn't theoretical: it prevents subtle contract violations that take down production.
Key Takeaway
Inheritance must follow LSP: subtypes must be fully substitutable.
If you can't substitute without breaking callers, use composition.
Code reuse is a side effect, not the goal of inheritance.

Composition Over Inheritance – A Real-World Refactoring

Many developers default to inheritance when they need to share code. But composition — assembling behaviour from smaller, focused classes — is often a better choice. The rule of thumb: 'Favor composition over inheritance.'

Consider a Bird class that needs to fly. If you create a FlyingBird subclass, you'll soon have NonFlyingBird, SwimmingBird, etc. Adding a new capability (like Sing) explodes the class hierarchy. Instead, compose the bird with a FlyBehavior interface and delegate.

Interviewers love this topic because it tests your ability to design flexible systems. When they ask 'How would you model a bird?' they're not looking for inheritance tree depth; they want to see if you reach for interfaces and delegation.

Here's a clean composition example: an OrderProcessor composed with a DiscountCalculator instead of extending a BaseOrder. This lets you swap discount strategies at runtime without changing the processor.

io/thecodeforge/oop/OrderProcessor.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package io.thecodeforge.oop;

// Discount as a separate responsibility
interface DiscountCalculator {
    double applyDiscount(double total);
}

class NoDiscount implements DiscountCalculator {
    @Override public double applyDiscount(double total) { return total; }
}

class SeasonalDiscount implements DiscountCalculator {
    @Override public double applyDiscount(double total) { return total * 0.9; }
}

public class OrderProcessor {
    private final DiscountCalculator discount;

    public OrderProcessor(DiscountCalculator discount) {
        this.discount = discount;
    }

    public double process(double total) {
        // ... validation, tax calculation etc.
        return discount.applyDiscount(total);
    }
}
Output
OrderProcessor composes discount logic. New discount strategies don't require new subclasses.
Think in Terms of Behaviors
  • Inheritance models identity; composition models capability.
  • Composition keeps classes small and focused (Single Responsibility).
  • You can swap behaviors at runtime (Strategy pattern).
  • Composition doesn't lock you into a rigid hierarchy.
  • Interfaces make composition natural and testable.
Production Insight
A product catalog system using deep inheritance (Animal > Mammal > FlyingMammal > Bat) became unmaintainable.
Adding 'EchoLocator' capability forced a new subclass for every existing mammal that could echo locate.
After refactoring to composition (Animal + EchoLocator interface), adding a new capability didn't touch existing code.
Key Takeaway
Favor composition over inheritance for flexible design.
Compose objects with interfaces for pluggable behavior.
Inheritance is for identity; composition is for capability.
● Production incidentPOST-MORTEMseverity: high

The Payment Processing Bug That Cost $23K

Symptom
Customers received refund confirmations but never saw the money. The refund method silently did nothing.
Assumption
The team assumed that adding a RefundablePayment subclass wouldn't break the existing process method because the parent Payment class seemed generic enough.
Root cause
The RefundablePayment class extended Payment and overrode process() — but the overridden version in the base class was called instead for non-refundable payments due to a missing @Override annotation and different method signature. More subtly, the code that called process() didn't know about refund(), and RefundablePayment was not truly substitutable for Payment without breaking the caller's expectation.
Fix
Extracted a Refundable interface for refund capability. RefundablePayment implemented both Payment and Refundable, and the refund logic was moved out of the inheritance chain. The process method was marked final in the base class to prevent accidental override shadowing.
Key lesson
  • Favor composition over inheritance when adding orthogonal behavior like refunds.
  • Always annotate overrides with @Override to catch signature mismatches at compile time.
  • If you can't guarantee Liskov Substitutability, break the inheritance and use interfaces.
Production debug guideWhen inheritance and polymorphism go wrong, here's how to trace the issue fast.4 entries
Symptom · 01
A method call behaves differently than expected (e.g., wrong implementation runs).
Fix
Check if the method is overridden or overloaded. Use a debugger or add System.out.println(getClass().getName()) to see the actual runtime type. Verify @Override annotations.
Symptom · 02
A subclass method is never called despite being defined.
Fix
Look for method visibility issues — if the parent method is private, it's not overridable. Mark it protected or public. Also check the constructor invocation order: if a parent constructor calls an overridden method, the child's fields may not yet be initialized.
Symptom · 03
An object's state gets corrupted by code that accessed fields directly.
Fix
Review Encapsulation: ensure all fields are private and accessed only via getters/setters. If a setter is missing validation, add it. Consider making the class final to prevent unintended subclass access.
Symptom · 04
A generic method that accepts supertype behaves incorrectly for some subtypes.
Fix
Test each subtype individually. If the method uses instanceof checks to branch behavior, it's a sign of violated LSP. Refactor by introducing a separate interface or abstract method.
★ Quick OOP Debug Cheat SheetCommon OOP symptoms and the first commands to diagnose them.
Method not found at runtime (NoSuchMethodError)
Immediate action
Check that the classpath includes the correct version of the library.
Commands
javap -p <classname> to list methods in the compiled class
mvn dependency:tree to see conflicting versions
Fix now
Exclude the unwanted version and rebuild.
Field value is null unexpectedly in a subclass+
Immediate action
Identify when the field is supposed to be set - during construction or later?
Commands
Add a breakpoint in the parent constructor and child constructor
Add `System.out.println(getClass().getSimpleName() + " " + field)`
Fix now
Initialize the field in the parent constructor if it's always needed.
Overloaded method instead of overridden method called+
Immediate action
Check parameter types - they must match exactly for overriding.
Commands
javap -c -p <classname> to see method signatures
Add `@Override` to ensure compile-time check
Fix now
Correct the method signature or add the annotation.
AspectAbstract ClassInterface
Can hold instance state (fields)Yes — instance fields allowedNo — only static final constants
ConstructorYes — can define constructorsNo — interfaces have no constructors
Inheritance limitSingle parent class onlyA class can implement unlimited interfaces
Method types allowedAbstract + concrete + staticAbstract + default + static (Java 8+)
Access modifiers on methodsAny modifier (private, protected, public)Public by default (private since Java 9)
Best used whenShared state + is-a relationship existsShared capability across unrelated classes
Real Java exampleAbstractList in java.utilComparable, Runnable, Serializable

Key takeaways

1
Encapsulation isn't about getters and setters
it's about protecting invariants so no external code can put your object into an illegal state.
2
Abstraction defines the contract (what must be done); polymorphism executes it (which version runs at runtime). They're partners, not synonyms.
3
Choose abstract class when subtypes share instance state or a genuine is-a relationship. Choose interface for capabilities that apply across unrelated class hierarchies.
4
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.
5
Composition over inheritance gives you runtime flexibility and keeps your class hierarchy flat.

Common mistakes to avoid

3 patterns
×

Confusing overloading with overriding

Symptom
Candidate says 'method overriding' when they mean overloading, or claims both are runtime polymorphism. Overloading is resolved at COMPILE time based on parameter types. Overriding resolved at RUNTIME based on actual object type.
Fix
Remember: overLOADing = LOts of signatures, resolved early. overRIDing = child RIDes over parent, resolved late. Use @Override annotation to enforce compile-time check.
×

Thinking private fields are 'inherited'

Symptom
Candidate says child classes inherit everything, including private fields. Private fields exist in child's memory but are NOT accessible by name in the child class. The child can only reach them via public/protected accessors from the parent.
Fix
Declare fields protected if child needs direct access, but prefer private + accessor methods to preserve encapsulation.
×

Using an interface purely because 'it allows multiple inheritance'

Symptom
Choosing interface over abstract class solely to avoid Java's single-class inheritance limit. If types share state and behaviour via is-a, forcing an interface means copying default implementations or creating awkward workarounds.
Fix
Ask: 'do these types share state?' If yes, abstract class. Then ask: 'do they need additional capabilities?' Add interfaces on top for those capabilities.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Explain the 'Diamond Problem' in Java. Why does it not occur with interf...
Q02SENIOR
Why is it said that 'Composition is better than Inheritance'? Provide a ...
Q03SENIOR
If a parent class constructor calls an overridden method, what is the ri...
Q01 of 03SENIOR

Explain the 'Diamond Problem' in Java. Why does it not occur with interfaces even with default methods, but would occur with multiple class inheritance?

ANSWER
The Diamond Problem occurs when a class inherits from two classes that have a common ancestor, leading to ambiguity about which method implementation to use. Java avoids this for classes because single inheritance is enforced. For interfaces with default methods, Java resolves conflicts by: (1) explicit override in the implementing class wins, (2) if not overridden, the most specific default method wins (from the interface closest to the implementing class), (3) if there's ambiguity, the class must override the method. This resolution is defined in Java Language Specification §9.4.1.3.
FAQ · 4 QUESTIONS

Frequently Asked Questions

01
What is 'Shadowing' vs 'Overriding' in Java?
02
Why does Java not support multiple inheritance of classes?
03
What is the difference between method overloading and method overriding in Java?
04
What is the difference between 'final' keyword on a method vs a class?
🔥

That's Java Interview. Mark it forged?

4 min read · try the examples if you haven't

Previous
Top 50 Java Interview Questions
2 / 6 · Java Interview
Next
Java Collections Interview Questions