OOP Principles the Interviewer Actually Probes
The OOP questions behind the textbook four: static vs dynamic dispatch, Liskov violations that compile cleanly, abstract class versus interface, and composition over inheritance.
On this page
Every candidate can recite encapsulation, inheritance, polymorphism and abstraction. That recitation takes ten seconds and tells the interviewer nothing, which is why the real questions live one layer down: which of these is decided by the compiler, which by the JVM, and what happens when a subclass quietly breaks its parent's promises.
Key Takeaways
- Overloading is resolved by the compiler from declared argument types. Overriding is resolved by the JVM from the runtime type of the receiver.
- Static methods and fields are never polymorphic — they are hidden, not overridden.
- A Liskov violation compiles perfectly.
Square extends Rectangleis the canonical case. - Interfaces gained behaviour with default methods; they still cannot hold instance state.
- "Composition over inheritance" is about avoiding a permanent coupling to a superclass's
implementation details, not about disliking
extends.
Two kinds of dispatch
class Animal {
static String kind() { return "animal"; }
String speak() { return "..."; }
String name = "generic";
}
class Dog extends Animal {
static String kind() { return "dog"; } // hides, does not override
@Override String speak() { return "woof"; } // overrides
String name = "rex"; // shadows, does not override
}
Animal a = new Dog();
a.speak(); // "woof" — runtime type wins
Animal.kind(); // "animal" — resolved from the declared type
a.name; // "generic" — fields are never polymorphicOnly the instance method is dispatched on the runtime type. Static methods are bound at compile time from the declared type, and fields are resolved the same way. Interviewers reach for this constantly because the code looks uniform and behaves in three different ways.
Overload selection is a compile-time decision too, and it uses the declared type of each argument:
void log(Object o) { System.out.println("object"); }
void log(String s) { System.out.println("string"); }
Object value = "hello";
log(value); // prints "object" — value is declared Object
log((String) value); // prints "string"The runtime class of value is String, and the compiler does not care. Once you can state that
cleanly — "overload resolution happens at compile time on static types; override resolution happens
at runtime on the receiver's actual class" — most follow-ups answer themselves.
Encapsulation is not "make fields private"
Private fields with public getters and setters for every one of them is not encapsulation — it is a public field with extra steps. Encapsulation is about controlling the invariants: no caller can put the object into a state it should not be in.
public class BankAccount {
private BigDecimal balance;
private final List<Transaction> history = new ArrayList<>();
// No setBalance(). The balance is a consequence of transactions,
// and every mutation goes through a rule.
public void withdraw(BigDecimal amount) {
if (amount.signum() <= 0) throw new IllegalArgumentException("amount must be positive");
if (balance.compareTo(amount) < 0) throw new InsufficientFundsException(balance, amount);
balance = balance.subtract(amount);
history.add(Transaction.debit(amount));
}
// Defensive: without the wrapper a caller could clear the audit trail.
public List<Transaction> history() {
return Collections.unmodifiableList(history);
}
}The interview question hiding here is "what is wrong with a getter that returns a mutable collection?" — the answer being that it hands out a live reference to internal state, letting a caller bypass every rule the class enforces.
Liskov, and the square that is not a rectangle
The Liskov Substitution Principle says a subtype must be usable anywhere its supertype is, without the caller noticing. Java's type system checks the signatures and nothing else, so a violation compiles cleanly and fails at runtime.
class Rectangle {
protected int width, height;
void setWidth(int w) { this.width = w; }
void setHeight(int h) { this.height = h; }
int area() { return width * height; }
}
class Square extends Rectangle {
@Override void setWidth(int w) { this.width = w; this.height = w; }
@Override void setHeight(int h) { this.width = h; this.height = h; }
}
// A test written against Rectangle, which every Rectangle must pass:
void resizeTest(Rectangle r) {
r.setWidth(5);
r.setHeight(4);
assert r.area() == 20; // fails for Square: area is 16
}Square satisfies the compiler and breaks the contract. Mathematically a square is a rectangle;
behaviourally, a mutable square is not a mutable rectangle, because it cannot honour independent
width and height. The lesson generalises: an override may weaken preconditions and strengthen
postconditions, never the reverse.
The three practical signals that you are about to violate LSP: an override that throws
UnsupportedOperationException, an override that ignores its arguments, and any caller that has to
check instanceof before deciding what to do.
Abstract class or interface
| Abstract class | Interface | |
|---|---|---|
| Instance state | Yes | No — only static final constants |
| Constructors | Yes | No |
| How many per class | One | Many |
| Method bodies | Yes | Yes, via default and static |
| Access modifiers | All | public, plus private helpers since Java 9 |
Since Java 8 an interface can carry behaviour, so the deciding question is no longer "does it need code?" but "does it need instance state, or a constructor?" If yes, it is an abstract class. If it is a capability that unrelated types might also want, it is an interface.
Java's own libraries use both together: AbstractList exists so an implementer overrides two methods
instead of twelve, while List remains the type everything is written against. That pairing — an
interface for the contract, an abstract skeleton for convenience — is the pattern worth naming in an
interview.
Composition over inheritance
Inheritance couples you to a superclass's implementation, not just its interface, and that coupling survives every future version of the superclass.
// Counts every element ever added. Looks correct.
class CountingSet<E> extends HashSet<E> {
private int added = 0;
@Override public boolean add(E e) { added++; return super.add(e); }
@Override public boolean addAll(Collection<? extends E> c) {
added += c.size();
return super.addAll(c); // HashSet.addAll calls add() internally...
}
}
CountingSet<String> s = new CountingSet<>();
s.addAll(List.of("a", "b", "c"));
s.added; // 6, not 3 — every element was counted twiceNothing in HashSet's public contract says whether addAll calls add. The subclass depends on an
undocumented internal detail, and a JDK upgrade that changes it silently changes the result.
Wrapping a Set in a field and delegating to it has no such exposure — the decorator only sees the
public API.
Use inheritance when the relationship is genuinely "is-a" and the superclass was designed for
extension: documented self-use, protected hooks, a stable contract. Otherwise compose.
What gets asked
The reliable sequence is: define the four pillars, then immediately "give me an example of polymorphism that is resolved at compile time", then the static-method-hiding puzzle, then abstract class versus interface, and finally something open-ended about inheritance in a codebase you have worked on. Have one real example ready for the last one — a place you chose composition and why. It converts a memorised answer into a credible one.
Frequently Asked Questions
Is overloading polymorphism?
When should I use an abstract class instead of an interface?
Can you override a static method?
Related tutorials
- Strings: Immutability, the Pool and StringBuilderWhy String is immutable and what that buys you, how the string pool and intern() really work, why == sometimes appears to work, compact strings, and when concatenation in a loop actually costs you.
- Exception Handling, finally and try-with-resourcesChecked versus unchecked and when each is right, how finally silently discards an exception or a return value, and suppressed exceptions in try-with-resources.
- The equals() and hashCode() ContractWhy overriding equals() without hashCode() breaks every hash-based collection, what the five contract rules guarantee, and the mutable-key bug that silently loses data.
- static, final and Class Initialisation OrderThe exact order the JVM initialises a class, why a static final String can survive deleting the class that declared it, effectively final, and how two classes can deadlock while loading.