Object-Oriented Programming Interview Questions & Answers

34 questions Updated 2026-06-18

Java OOP interview questions — the four pillars, inheritance vs composition, abstract classes vs interfaces, overloading vs overriding, polymorphism, the equals/hashCode contract, records and enums.

Read the in-depth guideJava OOP — Inheritance, Polymorphism, Abstraction & Encapsulation
  • Encapsulation — bundle data with the methods that operate on it and hide internal state behind a public API (private fields + getters/setters).
  • Abstraction — expose what an object does, hide how, via interfaces and abstract classes.
  • Inheritance — a subclass reuses and extends a superclass's members (extends).
  • Polymorphism — one reference type, many runtime forms; the actual object's overridden method is called.
class Animal { String sound() { return "..."; } }
class Dog extends Animal { String sound() { return "Woof"; } } // inheritance + override
Animal a = new Dog();   // polymorphism
a.sound();              // "Woof"

A good one-liner: encapsulation hides data, abstraction hides complexity, inheritance reuses behavior, polymorphism varies it.

Encapsulation means keeping fields private and exposing controlled access through methods, so an object protects its own invariants and you can change the internals without breaking callers.

class Account {
  private double balance;           // hidden state
  public void deposit(double amt) {
    if (amt <= 0) throw new IllegalArgumentException();
    balance += amt;                 // validated mutation
  }
  public double getBalance() { return balance; }
}

Benefits: validation in one place, freedom to refactor representation, easier debugging (state changes go through known methods), and thread-safety hooks. A public mutable field gives up all of these.

Abstraction is exposing a simplified contract while hiding implementation detail. In Java you achieve it with interfaces (pure contract) and abstract classes (partial implementation). Callers depend on the abstract type, not the concrete class.

interface PaymentGateway { void charge(int cents); }

class StripeGateway implements PaymentGateway {
  public void charge(int cents) { /* API calls hidden here */ }
}

PaymentGateway gw = new StripeGateway(); // code depends only on the contract

The payoff is decoupling: you can swap StripeGateway for PayPalGateway without touching the calling code — the basis of "program to an interface."

Inheritance lets a subclass acquire the fields and methods of a superclass with extends, modeling an is-a relationship and enabling reuse + polymorphism.

class Vehicle { void start() { } }
class Car extends Vehicle { void honk() { } } // Car is-a Vehicle

Java allows single inheritance of classes only — a class can extend exactly one superclass (to avoid the "diamond problem"). It can implement multiple interfaces, which is how Java gets multiple-type flexibility without multiple class inheritance. final classes can't be extended at all.

Polymorphism = "many forms." Java has two kinds:

  • Runtime (dynamic) polymorphism — method overriding. The JVM picks the method based on the actual object at runtime (dynamic dispatch).
  • Compile-time (static) polymorphism — method overloading. The compiler picks the method based on the declared argument types.
Animal a = new Dog();
a.sound();   // runtime: Dog's override is chosen

print(5);    // compile-time: print(int)
print("hi"); // compile-time: print(String)

"Polymorphism" in interviews usually means the runtime kind — the engine behind List<Shape> calling each shape's own area().

Overloading Overriding
What Same name, different parameters Subclass replaces a superclass method
Signature Must differ (type/count) Must be identical
Bound Compile time (static) Run time (dynamic)
Return type Can differ Same or covariant
class Calc {
  int add(int a, int b) { return a + b; }          // overload 1
  double add(double a, double b) { return a + b; }  // overload 2
}
class Base { void greet() { } }
class Sub extends Base { @Override void greet() { } } // override

Overloading is about offering variations of an operation; overriding is about specializing inherited behavior. Use @Override so the compiler verifies you actually overrode (and didn't accidentally overload).

  • Abstract class — can have state (fields), constructors, and a mix of concrete and abstract methods. A class extends one. Models an is-a with shared implementation.
  • Interface — a contract; historically only abstract methods, now also default/static/private methods, but no instance state (only public static final constants). A class implements many.
abstract class Shape {
  private String name;            // state allowed
  abstract double area();         // subclasses must implement
  String describe() { return name + ": " + area(); }
}
interface Drawable { void draw(); default void hide() { } }

Rule of thumb: use an interface for a capability multiple unrelated classes can have (Comparable, Runnable); use an abstract class when subclasses share state or implementation and form a tight family.

Inheritance (is-a) tightly couples a subclass to its parent's implementation; changes to the base can silently break subclasses (the "fragile base class" problem). Composition (has-a) builds behavior by holding other objects and delegating — looser coupling, 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 ("favor composition over inheritance"): use inheritance only for a true is-a with a stable base designed for extension; otherwise compose. It also sidesteps single-inheritance limits.

super refers to the immediate superclass. It does two jobs: call the parent constructor (super(args), must be the first statement) and call an overridden parent method or access a parent field (super.method()).

class Animal {
  Animal(String name) { }
  void describe() { System.out.println("an animal"); }
}
class Dog extends Animal {
  Dog(String name) {
    super(name);              // chain to parent constructor
  }
  @Override void describe() {
    super.describe();         // reuse parent behavior
    System.out.println("...specifically a dog");
  }
}

If you don't call super(...) explicitly, the compiler inserts a no-arg super() — which fails to compile if the parent has no no-arg constructor.

this is a reference to the current object. Uses: disambiguate a field from a same-named parameter, call another constructor in the same class (this(...), constructor chaining), and pass the current instance to other methods.

class Point {
  int x, y;
  Point(int x, int y) {
    this.x = x;          // field vs parameter
    this.y = y;
  }
  Point() { this(0, 0); } // chain to the other constructor
}

this(...) for constructor chaining, like super(...), must be the first statement — so you can't use both in one constructor.

A constructor initializes a new object. It has the class's name, no return type, and runs when you use new. If you write none, the compiler supplies a default no-arg constructor.

class User {
  String name;
  User() { this("anon"); }      // no-arg, chains
  User(String name) { this.name = name; } // parameterized
}

Rules interviewers probe: defining any constructor removes the free default one; constructors can be overloaded; they're not inherited (but a subclass must call one via super); and private constructors enable singletons and factory-only construction.

Construction runs top-down through the hierarchy:

  1. Static fields/blocks run once when the class first loads.
  2. On new: the superclass constructor runs first (via super), all the way up to Object.
  3. Then instance field initializers and instance initializer blocks run.
  4. Then the rest of the subclass constructor body.
class A { A() { System.out.println("A ctor"); } }
class B extends A {
  int x = init();
  int init() { System.out.println("B field"); return 1; }
  B() { System.out.println("B ctor"); }
}
// new B() prints: A ctor -> B field -> B ctor

A famous trap: calling an overridable method from a constructor runs the subclass override before the subclass's fields are initialized — so it may see null/0.

From most to least restrictive:

  • private — same class only.
  • (default / package-private) — same package only (no keyword).
  • protected — same package plus subclasses (even in other packages).
  • public — everywhere.
public class Api {
  private int secret;       // class only
  int packageScoped;        // package
  protected int forSubs;    // package + subclasses
  public int open;          // everyone
}

Default the most restrictive that works (usually private fields) — it minimizes coupling. Note protected also grants package access, which surprises people who think it's "subclasses only."

static binds a member to the class rather than an instance:

  • static field — one shared copy across all instances.
  • static method — called on the class, has no this, can't access instance members directly (utility methods, factories).
  • static nested class — doesn't hold a reference to an outer instance.
  • static block — runs once at class load for static setup.
class MathUtil {
  static final double PI = 3.14159;
  static int square(int n) { return n * n; } // MathUtil.square(5)
}

Because static methods aren't tied to an object, they can't be overridden (only hidden) and don't participate in runtime polymorphism.

  • final class — cannot be extended (e.g. String, Integer). Locks the design and lets the JIT optimize.
  • final method — cannot be overridden by subclasses; preserves critical behavior.
  • final field — assign-once (constant once set).
final class Constants { }          // no subclassing
class Base {
  final void critical() { }        // subclasses can't override
}

Common reasons: immutability (a class meant to be immutable is often final), security/invariant protection, and clarity of intent. It's the opposite of "open for extension," so use it deliberately.

If you override equals, you must override hashCode, obeying:

  1. Equal objects (a.equals(b)) must have equal hash codes.
  2. equals must be reflexive, symmetric, transitive, and consistent.
  3. Unequal objects may share a hash code (collisions are allowed).
class Point {
  final int x, y;
  @Override public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof Point p)) return false;
    return 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/HashSet may fail to find an object whose hashCode doesn't match its equals. Use Objects.hash(...) / Objects.equals(...) to implement them.

The default toString returns ClassName@hexHashCode — useless in logs. Override it to return a readable representation, which is automatically used in string concatenation, println, and debuggers.

class User {
  String name; int age;
  @Override public String toString() {
    return "User{name=" + name + ", age=" + age + "}";
  }
}
System.out.println(new User()); // User{name=null, age=0}

It's purely for human-readable diagnostics — don't parse it programmatically. IDEs and records can generate it for you.

A default method provides a body inside an interface using the default keyword. They were added in Java 8 so interfaces could evolve — e.g. adding forEach to Iterablewithout breaking every existing implementer.

interface Greeter {
  String name();
  default String greet() { return "Hello, " + name(); } // optional to override
}

If a class inherits conflicting defaults from two interfaces, it must override to resolve the ambiguity (it can call Iface.super.method()). Interfaces can also have static and private helper methods now — but still no instance state.

A marker interface has no methods — it just tags a class so that code (or the JVM) can detect a capability at runtime via instanceof. Classic examples: Serializable, Cloneable, RandomAccess.

class Config implements Serializable { } // "this class may be serialized"

The JVM checks if (obj instanceof Serializable) before serializing. Modern Java often prefers annotations for metadata, but marker interfaces still give you compile-time type checking and instanceof support that annotations don't.

An enum defines a fixed set of named instances. Unlike int constants, enums are type-safe, can have fields, constructors, and methods, and each constant can even override behavior.

enum Planet {
  EARTH(9.81), MARS(3.71);
  private final double gravity;
  Planet(double g) { this.gravity = g; }   // constructor
  double weight(double mass) { return mass * gravity; }
}
Planet.MARS.weight(70);  // type-safe, behavior attached

Enums are implicitly final singletons (one instance per constant), work in switch, provide values()/valueOf()/ordinal(), and are the recommended way to implement singletons.

A record (Java 16+) is a concise, immutable data carrier. From a one-line declaration the compiler generates a canonical constructor, private final fields, accessors, and equals/hashCode/toString.

record Point(int x, int y) { }

Point p = new Point(1, 2);
p.x();                       // accessor (no "get" prefix)
p.equals(new Point(1, 2));   // true — value-based

They're for transparent data aggregates (DTOs, value objects). Limits: records are final, can't extend a class (they implicitly extend Record), and all state is in the header — you can still add a compact constructor for validation.

  • Static nested class — declared static; no link to an outer instance, a namespaced helper.
  • Inner (non-static) class — holds an implicit reference to its enclosing instance; can access its outer fields.
  • Local class — declared inside a method.
  • Anonymous class — an unnamed inner class created and instantiated at once.
class Outer {
  static class Helper { }            // static nested
  class Inner { }                    // inner — needs an Outer instance
  Runnable r = new Runnable() {      // anonymous
    public void run() { }
  };
}

Gotcha: a non-static inner class keeps the outer object alive (potential memory leak), so prefer static nested classes unless you actually need the enclosing instance.

  • Identitya == b: are they the same object in memory?
  • Equalitya.equals(b): are they logically the same value, per the class's definition?
  • Some classes also expose ordering equivalence via compareTo == 0.
String a = new String("x"), b = new String("x");
a == b;            // false (identity)
a.equals(b);       // true  (equality)

BigDecimal x = new BigDecimal("1.0"), y = new BigDecimal("1.00");
x.equals(y);       // false! (scale differs)
x.compareTo(y) == 0; // true  (numeric equivalence)

BigDecimal is the classic gotcha where equals and compareTo disagree.

When overriding, the subclass method may return a subtype of the superclass method's return type — a covariant return. It lets overrides return more specific types without a cast.

class Animal { Animal reproduce() { return new Animal(); } }
class Dog extends Animal {
  @Override Dog reproduce() { return new Dog(); } // narrower return — legal
}
Dog puppy = new Dog().reproduce(); // no cast needed

Common in builder patterns and clone() overrides. (Parameters, by contrast, are not covariant — changing a parameter type makes it an overload, not an override.)

Static binding is resolved by the compiler from the declared type — used for static, private, final, and overloaded methods, plus fields. Dynamic binding is resolved at runtime from the actual object type — used for overridden instance methods (virtual dispatch).

class A { static void s() { } void v() { } int f = 1; }
class B extends A { static void s() { } @Override void v() { } int f = 2; }

A x = new B();
x.v();   // B.v()  — dynamic binding (overridden)
x.s();   // A.s()  — static binding (hidden, by declared type)
x.f;     // 1      — fields are static-bound, not polymorphic

Key insight: fields and static methods are hidden, not overridden — they bind to the declared type, which is a classic trick question.

Make the state impossible to change after construction:

  1. Mark the class final (no subclass can add mutability).
  2. Make all fields private final.
  3. Set everything in the constructor; provide no setters.
  4. Defensively copy mutable fields in and out (don't leak internal refs).
final class Range {
  private final int[] bounds;
  Range(int[] bounds) { this.bounds = bounds.clone(); } // copy in
  int[] bounds() { return bounds.clone(); }             // copy out
}

Immutables are inherently thread-safe, cacheable, and safe as map keys — String, Integer, and LocalDate are all built this way.

No. Both this(...) (chain to another constructor in the same class) and super(...) (chain to the parent) must be the first statement in a constructor — and there can only be one first statement, so you use at most one of them.

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

The pattern is to funnel overloaded constructors through this(...) down to one "primary" constructor that finally calls super(...).

  • new — the normal path, invokes a constructor.
  • Factory method — e.g. List.of(), Integer.valueOf(), your own Builder.build().
  • Class.getDeclaredConstructor().newInstance() — reflection.
  • clone() — copy an existing object.
  • Deserialization — reconstruct from bytes (bypasses constructors).
User a = new User();                          // new
List<Integer> b = List.of(1, 2);              // factory
User c = User.class.getDeclaredConstructor().newInstance(); // reflection

Note that reflection and deserialization can create objects without running a constructor, which is why immutability/singletons sometimes need extra guards.

  • is-a -> inheritance. A Car is a Vehicle -> class Car extends Vehicle.
  • has-a -> composition/aggregation. A Car has an Engine -> the Car holds an Engine field.
class Engine { }
class Car extends Vehicle {   // is-a Vehicle
  private final Engine engine = new Engine(); // has-a Engine
}

Modeling tip: if you can't truthfully say "X is a Y," don't use inheritance — reach for has-a (composition) instead. Misusing is-a (e.g. Stack extends Vector) is a classic design smell.

When a subclass declares a static method with the same signature as a parent static method, it hides rather than overrides it. The version called depends on the declared (compile-time) type, not the runtime object — the opposite of overriding.

class A { static String who() { return "A"; } }
class B extends A { static String who() { return "B"; } }

A ref = new B();
ref.who();   // "A"  — static method, resolved by declared type (hiding)
// If who() were an instance method, this would be "B" (overriding)

Because of this confusion, call static methods on the class (A.who()), never through an instance reference.

Yes. If two interfaces declare the same abstract method, one implementation satisfies both. But if they provide conflicting default methods, the class must override to break the tie, optionally delegating with Interface.super.method().

interface A { default String hi() { return "A"; } }
interface B { default String hi() { return "B"; } }

class C implements A, B {
  @Override public String hi() {
    return A.super.hi();   // explicitly choose A's default
  }
}

This is Java's controlled answer to the multiple-inheritance "diamond problem": multiple types are allowed, but conflicting behavior must be resolved explicitly.

By declaring variables and parameters as the interface/supertype, your code works with any implementation — the runtime calls the actual object's methods. This decouples callers from concrete classes.

void process(List<String> items) { }   // accepts ArrayList, LinkedList, List.of...
List<String> list = new ArrayList<>();  // swap impl freely
// list = new LinkedList<>();           // no caller changes needed

"Program to an interface, not an implementation" — depend on List, Map, Collection, not ArrayList/HashMap. It makes code testable (inject fakes) and flexible (change implementations without ripple effects).

  • Cohesion — how focused a class is on a single responsibility. High cohesion is good: a UserValidator that only validates users.
  • Coupling — how dependent classes are on each other's internals. Low coupling is good: classes interact through small, stable interfaces.
// low coupling: depends on an interface, not a concrete logger
class OrderService {
  private final Logger log;            // injected abstraction
  OrderService(Logger log) { this.log = log; }
}

The goal of good OO design is high cohesion, low coupling — classes that do one thing well and lean on each other as little as possible, which makes systems easier to change and test.

Five OO design principles for maintainable code:

  • S — Single Responsibility: a class has one reason to change.
  • O — Open/Closed: open for extension, closed for modification.
  • L — Liskov Substitution: subtypes must be usable wherever their base is.
  • I — Interface Segregation: prefer small, specific interfaces over fat ones.
  • D — Dependency Inversion: depend on abstractions, not concretions.
// Dependency Inversion: high-level code depends on an interface
interface Repository { void save(Order o); }
class OrderService {
  private final Repository repo;       // not a concrete DB class
  OrderService(Repository repo) { this.repo = repo; }
}

Liskov is the one interviewers probe most: a subclass that throws on, or weakens, an inherited method (the Square extends Rectangle problem) violates it.

Practice tests are coming soon

Get notified when interactive mock interviews and quizzes launch.