Java OOP Interview Questions — The $23K @Override Bug
A missing @Override caused a $23K refund bug in Java: non-refundable payments silently did nothing.
- 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
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.
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.
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.
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.
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.
- 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.
The Payment Processing Bug That Cost $23K
RefundablePayment subclass wouldn't break the existing process method because the parent Payment class seemed generic enough.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.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.- Favor composition over inheritance when adding orthogonal behavior like refunds.
- Always annotate overrides with
@Overrideto catch signature mismatches at compile time. - If you can't guarantee Liskov Substitutability, break the inheritance and use interfaces.
System.out.println(getClass().getName()) to see the actual runtime type. Verify @Override annotations.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.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.instanceof checks to branch behavior, it's a sign of violated LSP. Refactor by introducing a separate interface or abstract method.Key takeaways
Common mistakes to avoid
3 patternsConfusing overloading with overriding
Thinking private fields are 'inherited'
Using an interface purely because 'it allows multiple inheritance'
Interview Questions on This Topic
Explain the 'Diamond Problem' in Java. Why does it not occur with interfaces even with default methods, but would occur with multiple class inheritance?
Frequently Asked Questions
That's Java Interview. Mark it forged?
4 min read · try the examples if you haven't