Java · Fundamentals

Java OOP — Inheritance, Polymorphism, Abstraction & Encapsulation

6 min read Updated 2026-06-18

Practice Object-Oriented Programming interview questions

Object-oriented programming in Java

Java is an object-oriented language to its core, and OOP questions dominate Java interviews. Beyond reciting "the four pillars," what matters is understanding how the mechanisms work — dynamic dispatch, the equals/hashCode contract, why composition often beats inheritance, and where interfaces fit. This guide walks through the model and the design judgment that goes with it.

The four pillars

  • Encapsulation — bundle data with the methods that operate on it and hide state behind a public API (private fields + methods).
  • Abstraction — expose what an object does, hide how, via interfaces and abstract classes.
  • Inheritance — a subclass reuses and extends a superclass (extends).
  • Polymorphism — one reference type, many runtime forms.
class Account {
  private double balance;           // encapsulated state
  public void deposit(double amt) {
    if (amt <= 0) throw new IllegalArgumentException();
    balance += amt;                 // validated in one place
  }
}

Encapsulation lets you change internals without breaking callers; a public mutable field gives that up.

Inheritance and polymorphism

Inheritance models an is-a relationship with extends. Java allows single class inheritance (to avoid the diamond problem) but a class can implement many interfaces. Polymorphism comes in two forms:

  • Runtime (dynamic) — method overriding; the JVM calls the actual object's method via dynamic dispatch.
  • Compile-time (static) — method overloading; the compiler picks by argument types.
class Animal { String sound() { return "..."; } }
class Dog extends Animal { @Override String sound() { return "Woof"; } }
Animal a = new Dog();
a.sound(); // "Woof" — resolved at runtime by the real object

Overriding replaces an inherited method (same signature, dynamic); overloading offers variations with different parameters (resolved at compile time). Use @Override so the compiler verifies you actually overrode. A subtle point: fields and static methods are hidden, not overridden — they bind to the declared type, so A x = new B(); x.staticMethod() calls A's version.

Abstraction: interfaces vs abstract classes

  • Abstract class — can have state, constructors, and a mix of concrete and abstract methods; a class extends one. Use for a family of classes sharing implementation.
  • Interface — a contract; now allows default/static/private methods but no instance state; a class implements many. Use for a capability unrelated classes can share (Comparable, Runnable).
interface Drawable { void draw(); default void hide() {} }
abstract class Shape {
  abstract double area();
  String describe() { return "area=" + area(); }
}

Default methods (Java 8) let interfaces evolve without breaking implementers; if two interfaces give conflicting defaults, the class must override and can call Interface.super.method(). This is Java's controlled answer to multiple inheritance: multiple types are fine, conflicting behavior must be resolved explicitly.

Composition over inheritance

Inheritance tightly couples a subclass to its parent's implementation (the "fragile base class" problem). Composition — building behavior by holding other objects and delegating — is looser and more flexible.

// inheritance misused: a Stack is-a List? leaks all List methods
class Stack<T> extends ArrayList<T> {}

// composition: Stack HAS a list, exposes only stack operations
class Stack<T> {
  private final List<T> items = new ArrayList<>();
  void push(T t) { items.add(t); }
  T pop() { return items.remove(items.size() - 1); }
}

Guideline: use inheritance only for a genuine is-a with a base designed for extension; otherwise compose (has-a). This also sidesteps the single-inheritance limit.

Constructors, super, and this

A constructor initializes an object: same name as the class, no return type. Defining any constructor removes the free default one; constructors aren't inherited but a subclass must call one via super(...). this(...) chains to another constructor in the same class; both super(...) and this(...) must be the first statement, so you use at most one.

class Sub extends Base {
  Sub() { this(5); }       // delegate within Sub...
  Sub(int n) { super(n); } // ...which reaches super
}

Construction runs top-down: static initializers once at class load, then the superclass constructor, then instance field initializers, then the subclass constructor body. A classic trap: calling an overridable method from a constructor runs the subclass override before its fields are initialized.

The equals/hashCode contract

If you override equals, you must override hashCode, keeping them consistent: equal objects must have equal hash codes (and equals must be reflexive, symmetric, transitive, consistent).

class Point {
  final int x, y;
  @Override public boolean equals(Object o) {
    return o instanceof Point p && x == p.x && y == p.y;
  }
  @Override public int hashCode() { return Objects.hash(x, y); }
}

Break the contract and hash-based collections silently misbehave — a HashMap may fail to find an object whose hashCode doesn't match its equals. Base both on the same immutable fields.

Modern OOP features

  • record (Java 16+) — a concise, immutable data carrier that generates the constructor, accessors, equals/hashCode/toString.
  • enum — a type-safe fixed set of instances that can carry fields and methods.
  • Immutable classes — make the class final, fields private final, set in the constructor, no setters, and defensively copy mutable fields.
record Point(int x, int y) {}
enum Planet { EARTH(9.81), MARS(3.71);
  final double g; Planet(double g) { this.g = g; } }

Round it out with the SOLID principles — Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion — and aim for high cohesion, low coupling: classes that do one thing well and depend on abstractions, not concretions.

Recap

OOP in Java rests on encapsulation, abstraction, inheritance, and polymorphism — but the depth is in the mechanics: dynamic dispatch for overriding (vs hidden fields and statics), interfaces vs abstract classes for abstraction, and the equals/hashCode contract for collections. Favor composition over inheritance, chain constructors with this/super, and reach for records, enums, and immutable designs. Guided by SOLID and "high cohesion, low coupling," your Java designs stay flexible and correct.

Practice tests are coming soon

Get notified when interactive mock interviews and quizzes launch.