Home Interview Java OOP Interview Questions — The $23K @Override Bug
Intermediate 8 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 & Principal Engineer

20+ years shipping production code across the stack, with years spent interviewing engineers. Drawn from code that ran under real load.

Follow
Production
production tested
July 19, 2026
last updated
2,466
articles · all by Naren
Before you start⏱ 25 min
  • Solid grasp of fundamentals
  • Comfortable reading code examples
  • Basic production concepts
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
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
✦ Definition~90s read
What is Java OOP Interview Questions?

This article dissects Java OOP interview questions through the lens of a real production bug that cost $23,000 — a missed @Override annotation that silently broke polymorphic behavior. It’s not a list of trivia; it’s a deep dive into what interviewers actually probe: whether you understand OOP as a tool for managing complexity, not just a set of textbook definitions.

Think of a TV remote.

The four pillars (encapsulation, inheritance, polymorphism, abstraction) are explained with concrete Java examples, showing why each exists — for example, why encapsulation isn’t just private fields but a contract for state management, and why polymorphism without Liskov substitution leads to runtime surprises. The article targets mid-to-senior developers who’ve used OOP for years but may still confuse abstraction with polymorphism or choose inheritance over composition out of habit.

It contrasts abstract classes (shared state, partial implementation) with interfaces (behavioral contracts, multiple inheritance) using real-world scenarios like payment processors and logging frameworks. The refactoring section walks through replacing a brittle class hierarchy with composition, using a notification system as the case study — showing how to avoid the fragile base class problem that plagues many enterprise Java codebases.

If you’re preparing for a senior Java role or debugging a system where OOP principles have gone sideways, this article gives you the why behind the what.

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.

What Java OOP Interview Questions Actually Test

Java OOP interview questions assess whether you understand the four pillars—encapsulation, inheritance, polymorphism, and abstraction—not as textbook definitions, but as design tools that prevent production bugs. The core mechanic is that each pillar enforces a contract between components: encapsulation hides state behind methods, inheritance shares behavior across a hierarchy, polymorphism lets one interface serve multiple implementations, and abstraction separates what a system does from how it does it.

In practice, these principles interact in ways that matter at scale. For example, polymorphism via method overriding is O(1) at dispatch but can introduce subtle failures when a subclass changes behavior that a parent class's internal methods rely on. The infamous $23K bug occurred when a developer overrode a method without understanding that the parent class called it internally during initialization, causing a NullPointerException in production. This is why interviewers probe beyond syntax—they want to see if you grasp the runtime implications of each OOP choice.

Use OOP principles when you need to manage complexity across a team or over time. Encapsulation reduces ripple effects from refactoring; inheritance should be used only for true "is-a" relationships, not code reuse (prefer composition). Polymorphism enables pluggable architectures like strategy or observer patterns. In real systems, failing to apply these correctly leads to fragile code that breaks when extended, costing hours of debugging and, in extreme cases, revenue loss.

⚠ The Override Trap
A subclass overriding a method called inside the parent's constructor can crash the object before it's fully initialized—always mark such methods final or document the contract.
📊 Production Insight
A team added a logging override to a base class's getUserId() method; the parent constructor called getUserId() before the subclass fields were set, causing a NullPointerException in every new user session.
Symptom: intermittent NullPointerException on user login, only in production under load, because the timing of object creation varied.
Rule: never call overridable methods from constructors; if you must, document the contract and mark the method final.
🎯 Key Takeaway
Encapsulation is about hiding implementation, not just data—it prevents coupling that breaks when internals change.
Inheritance without Liskov substitution guarantees leads to runtime surprises; always verify that a subclass can replace its parent without altering correctness.
Polymorphism is powerful but dangerous when overridden methods are called from constructors or finalizers—document and enforce the call chain.
java-oop-interview-questions Java OOP Pillars Architecture Layered view of encapsulation, inheritance, polymorphism, abstraction Access Modifiers private | protected | public Encapsulation getters | setters | data hiding Inheritance extends | super | method overriding Polymorphism dynamic dispatch | interface binding Abstraction abstract classes | interfaces THECODEFORGE.IO
thecodeforge.io
Java Oop Interview Questions

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
java-oop-interview-questions Abstract Class vs Interface in Java When to use each for OOP design Abstract Class Interface Instantiation Cannot be instantiated Cannot be instantiated Method Implementation Can have both abstract and concrete meth All methods abstract (Java 8+ allows def Multiple Inheritance Class can extend only one abstract class Class can implement multiple interfaces Fields Can have instance variables Only static final constants Constructor Can have constructors No constructors allowed THECODEFORGE.IO
thecodeforge.io
Java Oop Interview Questions

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.
Mental Model
Think in Terms of Behaviors
Instead of asking 'What is this object?' ask 'What can this object do?'
  • 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.

Object Creation in Java — The Full Menu

Every Java developer knows 'new'. But if you're debugging a memory leak or designing a library that needs to control instantiation, you need the full picture. Here's what you can actually do: the new keyword (for normal objects), Class.forName().newInstance() (reflection, still used in legacy frameworks), Constructor.newInstance() (reflection with parameter access, preferred for modern reflective code), clone() (shallow copy — no constructor called, watch for shared mutable state), and deserialization (reads the object from a byte stream, also skips the constructor). Why does this matter? Because two of these mechanisms bypass constructor validation entirely. If your constructor sets invariants (e.g., 'age must be positive'), a cloned or deserialized object can violate them silently. The rule: never rely on a constructor alone for security. Validate invariants in setters or use a factory method that controls the entire creation path.

ObjectCreationExamples.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// io.thecodeforge
public class User {
    private final String name;
    private final int age;

    public User(String name, int age) {
        if (age < 0) throw new IllegalArgumentException("Age cannot be negative");
        this.name = name;
        this.age = age;
    }

    // Clone bypasses constructor!
    @Override
    public User clone() {
        return (User) super.clone(); // age could be -1
    }
}

// Usage in production:
User original = new User("Alice", 30);
User clone = original.clone(); // no validation here
Output
// No compile error. Violates invariant if original was corrupted.
⚠ Production Trap:
Serialization and cloning bypass constructors. If your object has validation in the constructor, you must implement readObject() or override clone() to re-validate. I've seen this create 'impossible' null pointer bugs in payment systems.
🎯 Key Takeaway
Always validate object state in a private validate() method called from constructor, setters, clone(), and readObject(). One validation point, zero surprises.

Access Modifiers — The Gatekeepers You Can't Ignore

Access modifiers are not a syntax quiz. They are your contract with the rest of the codebase. private means 'implementation detail — change at will'. public means 'other teams depend on this; break it and you get paged at 3 AM'. Here's the real-world breakdown: public — visible everywhere. Use only for API endpoints or stable library interfaces. protected — visible to subclasses and same package. This is the most abused modifier. Why? Because people use 'protected' for fields, thinking it's safe. It's not. It couples you to every subclass ever written. default (no modifier) — visible to package. Good for internal helpers you don't want to leak. private — only the class. Getters and setters should never be the default. Ask: 'does this state need to be exposed?' If not, keep it private. A senior move: use private for everything, then relax to default or protected only when you have a concrete use case. Never start with public.

AccessModifierRefactoring.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
// io.thecodeforge
public class PaymentService {
    private double rate;   // OK — internal state
    protected int timeout; // BAD — now every subclass inherits this field
    public String name;    // WORST — anyone can mutate your identity

    public PaymentService(double rate, int timeout, String name) {
        this.rate = rate;
        this.timeout = timeout;
        this.name = name;
    }
}

// After refactoring — defensive:
public class BetterPaymentService {
    private final double rate;
    private final int timeout;
    private String name;

    public BetterPaymentService(double rate, int timeout, String name) {
        this.rate = rate;
        this.timeout = timeout;
        this.name = name;
    }

    protected int getTimeout() { return timeout; } // controlled access
}
Output
// Subclass can read timeout via getter, but cannot mutate it.
⚠ Production Trap:
I've debugged a production outage where a subclass directly set a protected timeout field to 0, causing a denial-of-service. Getters and setters are not just Java ceremony — they are firewalls against accidental misuse.
🎯 Key Takeaway
Default to private final for fields. Expose behavior via methods, never raw fields. Relax visibility only when you have a provable need.

OOP Design Interview in Java: Parking Lot, Chess, Vending Machine

Object-oriented design interviews test your ability to model real-world systems using OOP principles. Common problems include designing a parking lot, a chess game, or a vending machine. The key is to identify classes, their relationships, and behaviors.

Parking Lot Design: Start with ParkingLot containing multiple Levels, each with ParkingSpots. Spots can be for different vehicle types (e.g., compact, large). Use inheritance: Vehicle abstract class with subclasses Car, Truck, Motorcycle. Implement parkVehicle(), removeVehicle(), and isFull(). Use enums for VehicleSize and SpotType. Ensure thread safety for concurrent access.

Chess Game: Model Board as an 8x8 grid of Piece objects. Piece is abstract with isValidMove() implemented by subclasses like King, Queen, Rook. Use enums for Color (WHITE, BLACK). Game class manages turns, check/checkmate logic. Consider using the Strategy pattern for move validation.

Vending Machine: States (Idle, Selecting, Dispensing) can be modeled with the State pattern. VendingMachine holds inventory (Map of Product to quantity) and a Payment interface (Cash, Card). Handle edge cases like insufficient change or sold-out items.

Focus on extensibility, encapsulation, and single responsibility. Interviewers look for clear class hierarchies, appropriate use of interfaces, and handling of edge cases.

ParkingLot.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
39
40
41
42
43
44
45
46
47
48
49
public enum VehicleSize { COMPACT, LARGE, MOTORCYCLE }

public abstract class Vehicle {
    protected String licensePlate;
    protected VehicleSize size;
    public abstract boolean canFitInSpot(ParkingSpot spot);
}

public class Car extends Vehicle {
    public Car(String license) {
        licensePlate = license;
        size = VehicleSize.COMPACT;
    }
    @Override
    public boolean canFitInSpot(ParkingSpot spot) {
        return spot.getSize() == VehicleSize.COMPACT || spot.getSize() == VehicleSize.LARGE;
    }
}

public class ParkingSpot {
    private VehicleSize size;
    private Vehicle currentVehicle;

    public ParkingSpot(VehicleSize size) { this.size = size; }
    public boolean isAvailable() { return currentVehicle == null; }
    public boolean park(Vehicle v) {
        if (!isAvailable() || !v.canFitInSpot(this)) return false;
        currentVehicle = v;
        return true;
    }
    public void removeVehicle() { currentVehicle = null; }
    public VehicleSize getSize() { return size; }
}

public class Level {
    private List<ParkingSpot> spots;
    public Level(int numSpots) {
        spots = new ArrayList<>();
        for (int i = 0; i < numSpots; i++) {
            spots.add(new ParkingSpot(VehicleSize.COMPACT));
        }
    }
    public boolean parkVehicle(Vehicle v) {
        for (ParkingSpot spot : spots) {
            if (spot.park(v)) return true;
        }
        return false;
    }
}
💡Interview Strategy
📊 Production Insight
In production, these designs often need to handle concurrency (e.g., multiple threads parking cars) and persistence (e.g., storing state in a database). Consider using locks or atomic operations.
🎯 Key Takeaway
OOP design interviews evaluate your ability to model real-world systems with clean class hierarchies, appropriate design patterns, and robust edge-case handling.

Composition vs Inheritance: Modern Best Practices

Inheritance establishes an "is-a" relationship, while composition establishes a "has-a" relationship. Modern best practices favor composition over inheritance because it offers greater flexibility, lower coupling, and easier testing.

Why Composition Wins: Inheritance exposes subclasses to parent implementation details, breaking encapsulation. Changes in the parent class can ripple through the hierarchy. Composition allows you to swap behaviors at runtime via delegation. For example, a Bird class can have a FlyBehavior interface, with CanFly and CannotFly implementations, instead of subclassing FlyingBird and NonFlyingBird.

When to Use Inheritance: Use inheritance only when there is a genuine hierarchical relationship and the subclass is a true subtype (Liskov Substitution Principle). For example, Square extends Rectangle is problematic because changing width independently violates LSP. Instead, use composition: Square contains a Rectangle or uses a Shape interface.

Example: Instead of Dog extends Animal, use Dog { private Animal animal; } if you need to reuse animal behaviors. Prefer interfaces for defining contracts. Use default methods in interfaces sparingly.

Modern Java: Records and sealed classes provide controlled inheritance. Records are implicitly final, encouraging composition. Sealed classes allow you to restrict which classes can extend them, making hierarchies more predictable.

Testing: Composition makes mocking easier because you can inject dependencies. Inheritance requires testing the entire hierarchy.

CompositionExample.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Inheritance approach (fragile)
class Animal {
    void eat() { System.out.println("Eating"); }
}
class Dog extends Animal { }

// Composition approach (flexible)
interface EatBehavior {
    void eat();
}
class CanEat implements EatBehavior {
    public void eat() { System.out.println("Eating"); }
}
class Dog {
    private EatBehavior eatBehavior;
    public Dog(EatBehavior eatBehavior) { this.eatBehavior = eatBehavior; }
    public void eat() { eatBehavior.eat(); }
}

// Usage
Dog dog = new Dog(new CanEat());
dog.eat();
⚠ Inheritance Pitfall
📊 Production Insight
In large codebases, composition reduces refactoring costs. For example, using the Strategy pattern (composition) allows you to add new behaviors without modifying existing classes.
🎯 Key Takeaway
Favor composition over inheritance for flexible, loosely coupled designs. Use inheritance only for true subtype relationships that satisfy LSP.

SOLID Principles with Java Code Examples

SOLID is a set of five design principles that make code more maintainable and scalable. Here's how they apply in Java:

Single Responsibility Principle (SRP): A class should have only one reason to change. Example: Employee class should not handle both payroll calculation and database persistence. Separate into Employee, PayrollCalculator, and EmployeeRepository.

Open/Closed Principle (OCP): Classes should be open for extension but closed for modification. Use interfaces or abstract classes. Example: Shape interface with area() method, and Circle, Rectangle implement it. Adding a new shape doesn't modify existing code.

Liskov Substitution Principle (LSP): Subtypes must be substitutable for their base types. Classic violation: Square extends Rectangle where setting width affects height. Instead, use a common Shape interface.

Interface Segregation Principle (ISP): Clients should not be forced to depend on interfaces they don't use. Split large interfaces into smaller, specific ones. Example: Worker interface with work() and eat(); separate into Workable and Eatable.

Dependency Inversion Principle (DIP): Depend on abstractions, not concretions. High-level modules should not depend on low-level modules. Use dependency injection. Example: NotificationService depends on MessageSender interface, not EmailSender directly.

Applying SOLID leads to testable, decoupled code. Use them as guidelines, not rigid rules.

SOLIDExamples.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
39
40
41
42
43
44
45
46
47
48
49
50
// SRP: Separate concerns
class Employee {
    private String name;
    // getters/setters
}
class PayrollCalculator {
    public double calculatePay(Employee e) { return 0; }
}

// OCP: Extend without modifying
interface Shape {
    double area();
}
class Circle implements Shape {
    private double radius;
    public double area() { return Math.PI * radius * radius; }
}

// LSP: Square does not extend Rectangle
interface Rectangle {
    void setWidth(int w);
    void setHeight(int h);
    int getArea();
}
class Square implements Rectangle {
    private int side;
    public void setWidth(int w) { side = w; }
    public void setHeight(int h) { side = h; }
    public int getArea() { return side * side; }
}

// ISP: Segregated interfaces
interface Workable { void work(); }
interface Eatable { void eat(); }
class Robot implements Workable {
    public void work() { }
}

// DIP: Depend on abstraction
interface MessageSender {
    void send(String message);
}
class EmailSender implements MessageSender {
    public void send(String message) { }
}
class NotificationService {
    private MessageSender sender;
    public NotificationService(MessageSender sender) { this.sender = sender; }
    public void notify(String msg) { sender.send(msg); }
}
🔥SOLID in Practice
📊 Production Insight
In production, SOLID reduces technical debt. For example, DIP enables easy swapping of implementations (e.g., mock services in tests) without changing high-level code.
🎯 Key Takeaway
SOLID principles guide you toward maintainable, scalable OOP designs. Each principle addresses a specific code smell like rigidity or fragility.
● 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
⚙ Quick Reference
10 commands from this guide
FileCommand / CodePurpose
iothecodeforgeoopBankAccount.java/**The Four Pillars
iothecodeforgeoopNotificationService.javainterface Notifier {Polymorphism vs Abstraction
iothecodeforgeoopVehicleRegistry.javaabstract class BaseVehicle {Abstract Classes vs Interfaces
iothecodeforgeoopPaymentProcessor.java/**Inheritance Pitfalls and the Liskov Substitution Principle
iothecodeforgeoopOrderProcessor.javainterface DiscountCalculator {Composition Over Inheritance – A Real-World Refactoring
ObjectCreationExamples.javapublic class User {Object Creation in Java
AccessModifierRefactoring.javapublic class PaymentService {Access Modifiers
ParkingLot.javapublic enum VehicleSize { COMPACT, LARGE, MOTORCYCLE }OOP Design Interview in Java
CompositionExample.javaclass Animal {Composition vs Inheritance
SOLIDExamples.javaclass Employee {SOLID Principles with Java Code Examples

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.
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?
N
Naren Founder & Principal Engineer

20+ years shipping production code across the stack, with years spent interviewing engineers. Drawn from code that ran under real load.

Follow
Verified
production tested
July 19, 2026
last updated
2,466
articles · all by Naren
🔥

That's Java Interview. Mark it forged?

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

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