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/privatemethods 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, fieldsprivate 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.