Skip to content

Java · Object-Oriented Programming

Java OOP — Inheritance, Polymorphism, Abstraction & Encapsulation

6 min read Updated 2026-06-18 Share:

Practice Inheritance 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.

More ways to practice

The self-quiz is live. Join our channel for updates, new content & tech tips.

Join our WhatsApp Channel