Skip to content

OOP Principles

OOP is built on four pillars: Encapsulation (bundling data and methods, hiding internals), Inheritance (creating new classes from existing ones), Polymorphism (same interface, different implementations), and Abstraction (exposing only essential features). These principles promote code reuse, modularity, and maintainability.


Encapsulation

Encapsulation bundles data (fields) and behavior (methods) into a single unit (class) and hides internal state behind well-defined interfaces. Access modifiers (private, protected, public) control visibility, and getters/setters expose only what's necessary. This prevents invalid state and makes refactoring safe.

Deep Dive: Access Modifiers
Modifier Class Package Subclass World
private
default (no modifier)
protected
public
Deep Dive: Encapsulation in Practice
public class BankAccount {
    private double balance; // Hidden from outside

    public void deposit(double amount) {
        if (amount > 0) {             // Validation enforces invariants
            this.balance += amount;
        }
    }

    public double getBalance() {
        return this.balance;           // Read-only access
    }

    // No setter — balance can only change via deposit/withdraw
}

Why it matters:

  • Prevents invalid state (e.g., negative balance without explicit logic)
  • Allows changing internal implementation without affecting callers
  • Enforces business rules in one place

Inheritance

Inheritance creates new classes from existing ones using extends (classes) or implements (interfaces). The subclass inherits methods and fields from the parent, enabling code reuse. However, deep inheritance hierarchies create tight coupling. Rule of thumb: Favor composition over inheritance (Effective Java, Item 18).

Deep Dive: Inheritance vs Composition

Inheritance ("is-a") creates tight coupling — changes in parent break children.

Composition ("has-a") is more flexible — delegate behavior to collaborators.

// Inheritance — tightly coupled
class Dog extends Animal { }

// Composition — loosely coupled, preferred
class Dog {
    private final Legs legs;   // Has legs (delegates movement)
    private final Tail tail;   // Has tail (delegates wagging)
}

When to use inheritance:

  • True "is-a" relationships (e.g., ArrayList is a List)
  • Template Method pattern (shared algorithm structure)
  • Framework extension points designed for it

When to use composition:

  • "Has-a" relationships
  • Behaviors that need to be swapped at runtime
  • When you need to combine behaviors from multiple sources
Deep Dive: The Diamond Problem

Java doesn't support multiple class inheritance (a class can't extend two classes) because of ambiguity — which parent method should be used?

// Java solves this with interfaces + default methods
interface Flyable {
    default void move() { System.out.println("Flying"); }
}
interface Swimmable {
    default void move() { System.out.println("Swimming"); }
}

class Duck implements Flyable, Swimmable {
    @Override
    public void move() {
        Flyable.super.move();  // Explicitly choose which default
    }
}

Polymorphism

Polymorphism lets the same interface have different implementations. Compile-time (static) polymorphism is achieved through method overloading. Runtime (dynamic) polymorphism is achieved through method overriding — the actual method called is determined by the object's runtime type, not the reference type. This is how Spring DI works: code programs to interfaces, the framework injects concrete implementations.

Deep Dive: Method Overloading vs Overriding
Feature Overloading Overriding
When resolved Compile-time Runtime
Same method name Yes Yes
Same parameters No (must differ) Yes (must match)
Same return type Not required Required (or covariant)
Annotation None @Override
Static methods Can overload Cannot override (hidden)
// Overloading — same name, different parameters
public int add(int a, int b) { return a + b; }
public double add(double a, double b) { return a + b; }

// Overriding — same signature, different class
class Animal {
    void speak() { System.out.println("..."); }
}
class Dog extends Animal {
    @Override
    void speak() { System.out.println("Woof!"); }
}

// Runtime polymorphism in action
Animal animal = new Dog();
animal.speak(); // Prints "Woof!" — Dog's version is called

Abstraction

Abstraction hides complexity and exposes only essential features. Achieved via abstract classes (partial implementation, can have state) and interfaces (pure contract, no state). Abstract classes are "is-a" with shared code; interfaces define capabilities ("can-do"). Since Java 8, interfaces can have default and static methods.

Deep Dive: Abstract Class vs Interface
Feature Abstract Class Interface
Methods Abstract + concrete Abstract + default + static
Fields Instance variables Constants only (public static final)
Constructors Yes No
Multiple inheritance No (single extends) Yes (multiple implements)
Access modifiers Any Public (methods are public by default)
Use case Shared code + state Define a capability/contract
// Abstract class — shared state and behavior
abstract class Shape {
    protected String color;
    Shape(String color) { this.color = color; }
    abstract double area();  // Must be implemented by subclass
    String getColor() { return color; }  // Shared behavior
}

// Interface — capability contract
interface Drawable {
    void draw();
    default void drawWithBorder() {  // Default method (Java 8+)
        draw();
        System.out.println("Drawing border");
    }
}

class Circle extends Shape implements Drawable {
    private double radius;
    Circle(String color, double radius) {
        super(color);
        this.radius = radius;
    }
    @Override double area() { return Math.PI * radius * radius; }
    @Override public void draw() { System.out.println("Drawing circle"); }
}

Common Interview Questions

Common Interview Questions
  • What are the four pillars of OOP?
  • Explain the difference between abstraction and encapsulation.
  • When would you use inheritance vs composition?
  • What is the diamond problem? How does Java solve it?
  • Can you achieve polymorphism without inheritance? (Yes — interfaces)
  • What is the difference between method overloading and overriding?
  • When would you use an abstract class vs an interface?
  • What is covariant return type?