Java Interview: OOP, SOLID & Design Patterns
The design round: polymorphism and abstraction explained the way interviewers want to hear it, SOLID applied to real code, the handful of patterns that get asked, immutability, and generics.
4 sections · ~32 min · 5-question quiz (pass ≥ 70%)
1The Four Pillars — Answered Well
Reciting definitions scores nothing. Show the why and give a concrete example.
Encapsulation — hide state, expose behaviour. Why: you can change internals without breaking callers.
public class Account {
private BigDecimal balance; // nobody can set this directly
public void withdraw(BigDecimal amount) { // invariants enforced in one place
if (amount.compareTo(balance) > 0) throw new InsufficientFundsException();
balance = balance.subtract(amount);
}
}
Abstraction — expose what, hide how. List is the abstraction; ArrayList is one implementation.
Inheritance — an "is-a" relationship for shared behaviour. Use it sparingly; favour composition over inheritance, because subclasses are coupled to their parent's implementation details.
Polymorphism — one interface, many behaviours. Distinguish the two kinds:
- Runtime (overriding): which method runs is decided by the object's type at execution time.
- Compile-time (overloading): which method is selected is decided by the declared argument types at compile time.
Shape s = new Circle(5);
s.area(); // Circle.area() — runtime dispatch on the actual object
Abstract class vs interface — the perennial question:
| Abstract class | Interface | |
|---|---|---|
| State | Can hold fields | Only static final constants |
| Constructors | Yes | No |
| Multiple inheritance | No | Yes |
| Use when | Sharing implementation among close relatives | Defining a capability many unrelated types can have |
Since Java 8 interfaces can carry default and static methods, so the practical answer is: interface by default; abstract class when you must share state.
2SOLID, With Code That Fails and Code That Passes
S — Single Responsibility. One reason to change. A class that parses a CSV, validates it, and writes to the DB changes for three reasons — split it.
O — Open/Closed. Open for extension, closed for modification. The tell is a growing if/else on type:
// Violates OCP — every new type edits this method
double fee(Payment p) {
if (p.type == CARD) return p.amount * 0.02;
if (p.type == UPI) return 0;
...
}
// Follows OCP — add a new strategy without touching existing code
interface FeePolicy { BigDecimal fee(BigDecimal amount); }
L — Liskov Substitution. A subtype must be usable wherever the base type is. The canonical violation: Square extends Rectangle — setWidth on a Square must also change the height, breaking every caller that assumes independence.
I — Interface Segregation. Many small interfaces beat one fat one. If implementers throw UnsupportedOperationException for half the methods, the interface is too big.
D — Dependency Inversion. Depend on abstractions, not concretions.
// Rigid — cannot be unit-tested without a real database
class OrderService { private final MySqlOrderRepo repo = new MySqlOrderRepo(); }
// Inverted — inject the abstraction; swap in a fake for tests
class OrderService {
private final OrderRepository repo;
OrderService(OrderRepository repo) { this.repo = repo; }
}
That last example is also the answer to "why do we use dependency injection?" — testability and swappable implementations. Constructor injection is preferred: it makes dependencies explicit and lets fields be final.
3The Patterns That Get Asked
Five patterns cover the overwhelming majority of Java interview questions.
Singleton — one instance. The safe implementations are the enum and the holder idiom:
public enum Config { INSTANCE; } // thread-safe, serialization-safe
public class Config { // lazy holder idiom
private Config() {}
private static class Holder { static final Config I = new Config(); }
public static Config getInstance() { return Holder.I; }
}
Be ready for "what's wrong with singletons?" — hidden global state, hard to test, hard to parallelise.
Factory — centralise object creation behind a method, so callers depend on the interface, not the concrete class.
Builder — for objects with many optional parameters; avoids telescoping constructors and enables immutability:
Pizza p = new Pizza.Builder().size(12).cheese(true).olives(true).build();
Strategy — swap an algorithm at runtime. In modern Java it is often just a lambda:
list.sort(Comparator.comparing(Employee::getName)); // Strategy, implicitly
Observer — publish/subscribe; one-to-many notification. Underlies event listeners and reactive streams.
Also worth a sentence each: Decorator (wrap to add behaviour — BufferedReader wrapping FileReader), Adapter (translate one interface into another), Template Method (base class fixes the skeleton, subclasses fill the steps).
How to answer a pattern question well: name the problem it solves, sketch five lines of code, then name a real class from the JDK or Spring that uses it. That third part is what most candidates skip.
4Immutability, Generics, and Modern Java
Immutable objects are thread-safe for free, safe as map keys, and cannot be corrupted by a caller.
public final class Money { // final: no subclass can break it
private final BigDecimal amount; // final fields
private final List<String> tags;
public Money(BigDecimal amount, List<String> tags) {
this.amount = amount;
this.tags = List.copyOf(tags); // defensive copy IN
}
public List<String> getTags() { return tags; } // already unmodifiable
}
The two things candidates forget: defensive copies of mutable inputs, and no setters. A final field holding an ArrayList is still mutable through its reference.
Records collapse all of that:
public record Money(BigDecimal amount, String currency) {}
Records give you final fields, a canonical constructor, equals, hashCode, and toString. You can add a compact constructor for validation.
Generics exist for compile-time type safety and to remove casts. The key interview point is type erasure: generics are checked at compile time and erased at runtime, so List<String> and List<Integer> are the same class at runtime, and you cannot write new T[10].
PECS — Producer Extends, Consumer Super:
void copy(List<? extends Number> src, // producer: we READ Numbers from it
List<? super Number> dst) // consumer: we WRITE Numbers into it
Modern Java worth name-dropping (it signals you have kept current):
var users = new ArrayList<User>(); // 10: local type inference
String s = """
text block"""; // 15
if (o instanceof User u && u.isActive()) {} // 16: pattern matching
sealed interface Shape permits Circle, Square {} // 17: exhaustive hierarchies