Skip to content
JavaAgentic

Type at least two characters. Try “RAG”, “pgvector” or “tool calling”.

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.

Beginner6 min readUpdated
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 Rectangle is 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

Dispatch.java
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 polymorphic

Only 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:

overload selection
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.

BankAccount.java
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.

the classic violation
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 classInterface
Instance stateYesNo — only static final constants
ConstructorsYesNo
How many per classOneMany
Method bodiesYesYes, via default and static
Access modifiersAllpublic, 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.

the fragile base class
// 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 twice

Nothing 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?
By the common Java definition, overloading is compile-time or static polymorphism and overriding is runtime or dynamic polymorphism. The distinction that matters is that the compiler picks the overload from the declared types of the arguments, while the JVM picks the override from the runtime type of the receiver. If an interviewer disputes the terminology, describe the mechanism instead — it is what they are actually testing.
When should I use an abstract class instead of an interface?
Use an abstract class when subclasses share mutable state or constructor logic, since interfaces cannot hold instance fields. Use an interface for capability and for anything a class might need alongside another supertype, since a class can implement many interfaces but extend one class. Default methods narrowed the gap for behaviour, but not for state.
Can you override a static method?
No. A static method belongs to the class, so redeclaring it in a subclass hides it rather than overriding it, and which one runs is decided by the declared type at compile time. The same applies to fields — fields are never polymorphic. This is a favourite trick question because the code compiles and produces a result most people predict wrongly.

Related tutorials