Low-Level Design (LLD): Master Object-Oriented Programming (OOP) in Record Time
This comprehensive guide covers Object-Oriented Programming (OOP) tailored specifically for Low-Level Design (LLD) technical interviews. It synthesizes fundamental abstractions, language mechanics, class relationship diagrams, design traps, and interview edge cases into structured study notes.
1. Why Do We Need OOP? (Classes & Objects)
Object-Oriented Programming (OOP) is a programming paradigm that organizes code around real-world entities (objects) rather than actions or pure logic. By modularizing complex systems into discrete, self-contained components, OOP facilitates scalability, reusability, and long-term maintainability.
Core Mental Model & Analogy
Real-Life Analogy: The Car Factory
Imagine a car manufacturing plant:
- Class (Blueprint): The architectural design and engineering schematic of a car. It specifies what attributes a car should have (color, engine type, brand) and what behaviors it can perform (accelerate, brake, start engine). The blueprint itself cannot be driven on the road; it is purely a structural template.
- Object (Concrete Instance): The physical car manufactured from the blueprint (e.g., a Red Hyundai i20 or a Black Honda Civic). It occupies physical memory (or road space) and holds concrete, unique state values.
Class Anatomy
A class fundamentally consists of three structural parts:
- Attributes (Fields / State): Data stored inside the instance.
- Methods (Behavior): Operations or logic that act upon the object's state.
- Constructor: A specialized block executed to initialize the instance's state upon creation.
// Definition of a Class (Blueprint)
public class Car {
// Attributes
private String brand;
private String color;
private int engineCapacity;
// Constructor
public Car(String brand, String color, int engineCapacity) {
this.brand = brand;
this.color = color;
this.engineCapacity = engineCapacity;
}
// Behaviors (Methods)
public void startEngine() {
System.out.println(brand + " engine started.");
}
public void displayInfo() {
System.out.println("Brand: " + brand + ", Color: " + color + ", Engine: " + engineCapacity + "cc");
}
}
// Instantiating Objects (Concrete Instances)
public class Main {
public static void main(String[] args) {
Car car1 = new Car("Hyundai", "Red", 1200);
Car car2 = new Car("Honda", "Black", 1500);
car1.displayInfo(); // Output: Brand: Hyundai, Color: Red, Engine: 1200cc
car2.displayInfo(); // Output: Brand: Honda, Color: Black, Engine: 1500cc
}
}
2. Constructors and Memory Mechanics
A Constructor is a special method block invoked automatically at runtime when an instance of a class is allocated in memory using the new keyword. Its primary responsibility is to transition an object from an uninitialized state into a valid initial state.
Key Structural Properties
- Shares the exact same name as the class.
- No return type, not even
void. - Executed automatically upon memory allocation.
Constructor Types & Behaviors
1. Default (No-Argument) Constructor
If no constructor is defined explicitly in a class, the Java compiler automatically inserts an invisible, zero-argument default constructor at compile time.
- Primitive Default Values:
int0,double0.0,booleanfalse. - Reference Default Values: Objects/Strings
null.
public class Movie {
String title; // Defaults to null
int duration; // Defaults to 0
}
// Memory Allocation:
Movie m = new Movie(); // Invisible default constructor invoked.
Note: If you define any custom parameterized constructor in your class, the compiler will not auto-generate the invisible default constructor. Attempting to call
new Movie()without explicitly declaring a no-arg constructor will result in a compilation error.
2. Parameterized Constructor
Allows explicit initialization of instance fields with caller-provided values during creation.
public class Movie {
private String title;
private int duration;
// Explicit Parameterized Constructor
public Movie(String title, int duration) {
this.title = title;
this.duration = duration;
}
}
3. Copy Constructor
Creates a new object instance by copying fields from an existing instance of the same class.
public class Movie {
private String title;
private int duration;
// Standard Parameterized Constructor
public Movie(String title, int duration) {
this.title = title;
this.duration = duration;
}
// Copy Constructor (Deep Copy Strategy)
public Movie(Movie other) {
this.title = other.title;
this.duration = other.duration;
}
}
Deep Copy vs. Shallow Reference Assignment
Movie original = new Movie("Inception", 148);
// Reference Assignment (NOT a copy):
// Modifying 'refCopy' will mutate 'original' because both variables point to the same heap address.
Movie refCopy = original;
// True Copy Instantiation via Copy Constructor:
// Modifying 'deepCopy' WILL NOT mutate 'original'.
Movie deepCopy = new Movie(original);
4. Private Constructor
Restricts object creation from outside the host class. This is used in Creational Design Patterns such as the Singleton Pattern or Utility classes containing only static methods.
public class Singleton {
private static Singleton instance;
// Private Constructor prevents external instantiation via `new Singleton()`
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
Interview Q&A: Deep Constructor Traps
Q1: Can a constructor be final, static, or abstract?
final: No. Constructors are not inherited by subclasses, so method overriding semantics do not apply to them. Marking themfinalis invalid.static: No. Constructors belong to an instance lifecycle during memory initialization (new).staticmembers belong to class metadata level.abstract: No. Abstract methods demand dynamic implementation by subclasses. Constructors must execute concrete initialization of the immediate class.
Q2: What is the sequence of constructor calls when an object of a subclass is instantiated?
Parent class constructors execute first, followed by subclass constructors. The compiler automatically inserts super() as the very first line of a subclass constructor if no explicit super(...) or this(...) call is present.
Q3: Can a constructor be marked synchronized?
No. Object-level synchronization locks an instance (this). Since the object is actively being created during constructor execution, no other thread can possess a reference to it yet. Synchronizing a constructor is syntactically invalid in Java.
Q4: Can a constructor contain a return statement?
Yes, but with a strict condition: It can use a bare return; statement to exit execution early (e.g., validation failure), but it cannot return a value (return x; causes a compilation error).
public class UserAccount {
private String username;
public UserAccount(String username) {
if (username == null) {
System.out.println("Invalid Username. Aborting initialization.");
return; // Valid early exit
}
this.username = username;
}
}
3. Disambiguation and Context with the this Keyword
The this keyword in Java is an explicit reference variable that holds the memory address of the current executing object instance.
Core Use Cases
1. Shadowing / Disambiguating Field References
Differentiates instance field names from method or constructor parameters when names collide.
public class Person {
private String name;
public Person(String name) {
// 'this.name' refers to the instance attribute
// 'name' refers to the constructor argument variable
this.name = name;
}
}
2. Constructor Chaining (this())
Invokes another constructor in the same class to avoid code duplication across overloaded constructors. this(...) must be the very first statement inside the calling constructor.
public class Person {
private String name;
private int age;
public Person(String name) {
// Delegates call to the 2-arg constructor
this(name, 0);
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
3. Method Chaining (Fluent Interface / Builder Pattern)
Returns the current instance reference (return this;) from setter methods to allow chained operation invocations.
public class Person {
private String name;
private int age;
public Person setName(String name) {
this.name = name;
return this; // Return current instance
}
public Person setAge(int age) {
this.age = age;
return this; // Return current instance
}
}
// Usage in caller:
Person p = new Person();
p.setName("Alice").setAge(30); // Method Chaining
4. Passing the Current Instance as a Parameter
Passes the current object reference to an external helper function or component.
public class EventLogger {
public void log(Person p) {
System.out.println("Processing person: " + p.getName());
}
}
public class Person {
private String name;
public String getName() { return name; }
public void register() {
EventLogger logger = new EventLogger();
logger.log(this); // Pass self reference
}
}
Crucial Rule: The
thiskeyword cannot be referenced insidestaticcontext (static methods or static blocks) because static constructs belong to the class level and exist independently of any instance.
4. Pillar 1: Polymorphism (Compile-Time vs. Run-Time)
Polymorphism (Poly = Many, Morph = Form) enables a unified interface to trigger distinct, context-specific behaviors depending on the calling context or the target runtime instance.
Compile-Time Polymorphism (Method Overloading)
Method Overloading occurs when multiple methods within the same class share the exact same method name but possess different parameter signatures (either by parameter count, parameter types, or parameter order).
Mental Analogy
Think of overloading like a heavy truck cargo bed: You load the same truck bed with varying configurations or quantities of payload depending on the shipment request.
public class Printer {
// Overload 1: Single String parameter
public void print(String text) {
System.out.println("Printing text: " + text);
}
// Overload 2: Single int parameter (Different Type)
public void print(int number) {
System.out.println("Printing number: " + number);
}
// Overload 3: Two parameters (Different Count)
public void print(String text, int copies) {
System.out.println("Printing " + copies + " copies of: " + text);
}
}
Trap Rule: Changing only the return type or access modifier of a method does not constitute method overloading. The parameter signature must change. Changing return types alone yields a compile error.
Run-Time Polymorphism (Method Overriding)
Method Overriding occurs when a subclass provides a dynamic, specialized implementation for a method already declared in its parent superclass or interface. Resolution is performed dynamically at runtime by the Java Virtual Machine (JVM) based on the actual target instance type in memory, rather than the reference variable type.
public class Vehicle {
public void start() {
System.out.println("Vehicle is starting...");
}
}
public class Car extends Vehicle {
@Override
public void start() {
System.out.println("Car starts with keyless ignition.");
}
}
public class Bike extends Vehicle {
@Override
public void start() {
System.out.println("Bike starts with a kick start.");
}
}
Dynamic Method Dispatch in Action
public class Main {
public static void main(String[] args) {
// Reference variable is parent type (Vehicle), Object in heap is child type (Car)
Vehicle myVehicle = new Car();
myVehicle.start(); // Output: "Car starts with keyless ignition."
myVehicle = new Bike();
myVehicle.start(); // Output: "Bike starts with a kick start."
}
}
Overloading vs. Overriding Comparison
5. Pillar 2: Inheritance & Structural Mechanics
Inheritance establishes an is-a relationship between a parent superclass and a child subclass, enabling child classes to reuse, extend, or override attributes and behaviors from parent classes.
Supported Types of Inheritance in Java
1. Single Inheritance
Parent
|
Child
2. Multi-Level Inheritance
GrandParent
|
Parent
|
Child
3. Hierarchical Inheritance
Parent
/ \
ChildA ChildB
- Single Inheritance: A single subclass extends a single superclass.
- Multi-Level Inheritance: A class extends a child class, forming a chain (
C extends B extends A). - Hierarchical Inheritance: Multiple subclasses extend the same parent class (
B extends AandC extends A).
Multiple Class Inheritance & The Diamond Problem
Java does not support multiple class inheritance (a single class extending multiple superclasses like class C extends A, B). This restriction eliminates structural ambiguity known as the Diamond Problem.
Code Scenario (Why Multiple Class Inheritance is Blocked)
// Conceptual Illegal Syntax in Java:
class Dog {
void makeSound() { System.out.println("Bark"); }
}
class Cat {
void makeSound() { System.out.println("Meow"); }
}
// ILLEGAL: If permitted, calling hybrid.makeSound() causes unresolvable ambiguity.
class HybridAnimal extends Dog, Cat {
}
The Java Solution: Multiple Interface Inheritance
Java completely avoids the Diamond Problem by supporting multiple inheritance strictly through interfaces. Interfaces enforce behavioral contracts without holding ambiguous concrete instance states.
interface Dog {
void makeSound();
}
interface Cat {
void makeSound();
}
// Legal: HybridAnimal provides explicit, unambiguous implementation
class HybridAnimal implements Dog, Cat {
@Override
public void makeSound() {
System.out.println("Hybrid growl!");
}
}
6. Pillar 3: Encapsulation & Data Integrity
Encapsulation is the practice of bundling data (fields) and operational logic (methods) together into a single unit (class), while restricting direct external access to internal implementation details.
Real-Life Analogy: The Medical Capsule
Think of a medical capsule: The active medicinal chemical powder is encapsulated safely inside an outer gelatin coating. Consumers do not interact directly with the loose internal powder; they swallow the intact capsule casing.
Encapsulation Code Implementation
public class BankAccount {
// Hidden private state prevents unvalidated direct mutation
private double balance;
public BankAccount(double initialBalance) {
if (initialBalance >= 0) {
this.balance = initialBalance;
} else {
this.balance = 0;
}
}
// Controlled Read Access (Getter)
public double getBalance() {
return this.balance;
}
// Controlled Write Access (Setter with Data Validation)
public void deposit(double amount) {
if (amount > 0) {
this.balance += amount;
System.out.println("Successfully deposited: $" + amount);
} else {
System.out.println("Invalid deposit amount!");
}
}
public void withdraw(double amount) {
if (amount > 0 && amount <= balance) {
this.balance -= amount;
System.out.println("Successfully withdrew: $" + amount);
} else {
System.out.println("Insufficient funds or invalid withdrawal!");
}
}
}
7. Pillar 4: Abstraction (Abstract Classes vs. Interfaces)
Abstraction exposes what an object does while hiding how it achieves its internal operational execution. It focuses on functional contracts rather than concrete implementation mechanics.
Real-Life Analogy: Driving a Vehicle
When you sit in a car, you interact directly with high-level abstraction controls: the steering wheel, accelerator pedal, and brake. You do not need to understand internal combustion thermodynamics, fuel injectors, or hydraulic line fluid mechanics to drive the car.
Abstract Classes vs. Interfaces
1. Abstract Class
An abstract class serves as a partial template. It cannot be instantiated directly using new and can contain both abstract methods (without bodies) and concrete methods (with implementation bodies), as well as instance variables and constructors.
public abstract class Animal {
private String name;
// Abstract classes can have constructors
public Animal(String name) {
this.name = name;
}
// Abstract Method (Must be overridden by concrete subclasses)
public abstract void makeSound();
// Concrete Method (Shared logic inherited directly)
public void sleep() {
System.out.println(name + " is sleeping. Zzz...");
}
}
public class Dog extends Animal {
public Dog(String name) {
super(name);
}
@Override
public void makeSound() {
System.out.println("Woof! Woof!");
}
}
2. Interface
An interface represents a pure behavioral contract.
- Prior to Java 8, interfaces could only declare abstract methods and
public static finalconstants. - Java 8+ Enhancements: Interfaces can include
defaultmethods (providing fallback concrete logic) andstaticutility methods. - Java 9+ Enhancements: Interfaces can include
privatemethods to encapsulate shared helper logic between default methods.
public interface Flyable {
// Constant (public static final by default)
int MAX_ALTITUDE = 10000;
// Abstract method contract
void fly();
// Java 8+ Default Method (Optional for implementers to override)
default void logTakeoff() {
logSystem("Takeoff initiated.");
}
// Java 9+ Private Method (Internal contract helper)
private void logSystem(String message) {
System.out.println("SYSTEM LOG: " + message);
}
}
Comparison: Abstract Class vs. Interface
Deep-Dive Interview Q&A on Abstraction
Q1: Why can't an interface have a constructor?
Constructors initialize instance state attributes stored in heap memory. Interfaces focus strictly on behavior contracts and cannot hold non-static instance fields. Because there is no state to initialize, interface constructors are unnecessary and strictly forbidden.
Q2: What happens if a class implements two interfaces that contain identical default method signatures?
This triggers a compile-time ambiguity conflict (a variation of the Diamond Problem). The implementing class must explicitly override the colliding method to clarify which implementation to execute.
interface InterfaceA {
default void execute() { System.out.println("A"); }
}
interface InterfaceB {
default void execute() { System.out.println("B"); }
}
public class Component implements InterfaceA, InterfaceB {
// Compilation Error if 'execute()' is not explicitly overridden
@Override
public void execute() {
// Disambiguation Option 1: Provide clean custom implementation
System.out.println("Custom Component Logic");
// Disambiguation Option 2: Explicitly target specific parent default
InterfaceA.super.execute();
}
}
Q3: What takes precedence if a class extends a Superclass AND implements an Interface with identical default method signatures?
The Class Rule Wins: Superclass method implementations always take higher priority over default interface methods.
public class Parent {
public void show() { System.out.println("Parent Class Show"); }
}
public interface Feature {
default void show() { System.out.println("Interface Feature Show"); }
}
public class Child extends Parent implements Feature {
// Calling child.show() prints: "Parent Class Show"
}
8. Access Modifiers & Visibility Scope
Access modifiers govern the visibility boundaries and access authorization levels of classes, fields, constructors, and methods across packages.
Scope Hierarchy Matrix
Practical Application in Low-Level Design
package com.lld.library;
public class LibraryItem {
public String title; // Globally visible
protected String internalId; // Subclasses across packages can access
String packageCode; // Accessible strictly within 'com.lld.library'
private double replacementCost; // Encapsulated within LibraryItem class only
}
private: Essential for encapsulating instance fields and sensitive internal helper routines.default(Package-Private): Useful for package-internal helper utilities (e.g., package-level parsers) that should remain hidden from external SDK consumers.protected: Used heavily in Framework designs (e.g., Template Method Pattern) where base class logic must be extended by sub-packages while shielding methods from global public access.public: Applied to public API entry points, interfaces, and primary domain model classes.
9. Class Relationship Diagrams (UML Concepts)
In LLD interviews, mapping structural relationships cleanly using standardized visual notations or text flows is critical.
The 6 Essential Structural Relationships
1. Inheritance (is-a)
A subclass extending a superclass.
Dog Animal
2. Realization (implements)
A class implementing an interface contract.
CreditCardPayment PaymentProcessor
3. Association (knows-a / has-a)
A standard directional or bi-directional structural link between two independent classes.
Person Car (A person owns/drives a car; both lifecycles exist independently).
4. Aggregation (Weak has-a)
A specialized association representing a whole-part container relationship where the child parts can exist independently if the parent container is destroyed.
Team Player (If the Team entity is dissolved, individual Player entities continue to exist).
5. Composition (Strong has-a)
A strict whole-part container relationship where the child parts cannot exist independently of the parent container lifecycle.
House Room (If the House is destroyed, the internal Room instances are destroyed along with it).
6. Dependency (uses-a)
A transient, short-term structural connection where one class temporarily relies on another, typically passed as a method local variable or parameter during single execution scope.
Document Printer (A Document uses a Printer object inside a print() method call, but does not retain a persistent long-term field reference to it).
10. Generics & Wildcards (?, ? extends T, ? super T)
Generics enforce compile-time type safety, eliminating runtime ClassCastException bugs and avoiding explicit manual casting boilerplate.
Generic Classes and Methods
// Generic Repository Class
public class DataBox<T> {
private T content;
public void setContent(T content) {
this.content = content;
}
public T getContent() {
return this.content;
}
}
// Usage:
DataBox<String> stringBox = new DataBox<>();
stringBox.setContent("LLD Notes"); // Safe
// stringBox.setContent(100); // Triggers Compile Error immediately!
Variance with Wildcards (?)
Wildcards allow flexible parameterized collection processing.
1. Unbounded Wildcard (List<?>)
Represents an unknown type. Suitable when methods perform strictly type-agnostic operations (e.g., printing list sizes or reading raw objects).
2. Upper-Bounded Wildcard (List<? extends Number>) — Covariance
Restricts acceptable types to a specific class or its subclasses.
- Primary Utility: Safe for reading elements from a collection (Producer).
- Write Constraint: Forbidden to write/add items (except
null) because the compiler cannot verify the concrete underlying heap storage type.
public static double sumOfList(List<? extends Number> list) {
double sum = 0.0;
for (Number n : list) {
sum += n.doubleValue(); // Safe Read
}
// list.add(10); // COMPILE ERROR: Cannot write safely to Upper-Bounded Wildcard!
return sum;
}
3. Lower-Bounded Wildcard (List<? super Integer>) — Contravariance
Restricts acceptable types to a specific class or its superclasses.
- Primary Utility: Safe for writing/inserting elements into a collection (Consumer).
public static void addIntegers(List<? super Integer> list) {
list.add(10); // Safe Write
list.add(20); // Safe Write
// Object item = list.get(0); // Reading returns raw Object; requires explicit casting
}
The PECS Mnemonic Principle
P.E.C.S: Producer Extends, Consumer Super.
- If your generic structure strictly produces data to be read from, use
? extends T.- If your generic structure strictly consumes data to be written into, use
? super T.
Comparison: Generics vs. Wildcards
11. Summary and Key Takeaways
- Classes vs. Objects: A Class is an un-instantiated logical blueprint defining attributes and operations. An Object is a physical instance residing in heap memory with unique concrete state values.
- Constructors: Used to initialize instances to a valid state. Custom parameterized constructors suppress the automatic generation of default zero-arg constructors.
- The
thisKeyword: Refers strictly to the active instance. It cannot be used inside static methods. Useful for constructor chaining (this()) and fluent method chaining (return this;). - Polymorphism Dual Nature:
- Compile-Time (Overloading): Same method name, different parameter signature within the same class.
- Run-Time (Overriding): Subclass replaces parent implementation; dynamic dispatch is resolved by the JVM at runtime based on the actual heap object type.
- Multiple Class Inheritance Prohibition: Java prevents extending multiple classes to avoid the Diamond Problem. Multiple inheritance is achieved safely using Interfaces.
- Encapsulation: Protects internal object state by declaring fields
privateand exposing controlled access via validated getters and setters. - Abstraction Constructs:
- Abstract Classes: Used for shared hierarchical state and behavior (
is-a). - Interfaces: Used for pure behavioral capabilities (
can-do). Java 8+ introducesdefaultmethods to support backward compatibility.
- Abstract Classes: Used for shared hierarchical state and behavior (
- Access Modifier Hierarchy: From most restrictive to least restrictive:
privatedefault(package-private)protectedpublic. - Class Relationships: Differentiate between Aggregation (weak
has-a, independent part lifecycles likeTeam-Player) and Composition (stronghas-a, dependent part lifecycles likeHouse-Room). - Generics & PECS Rule: Generics guarantee compile-time type safety. Remember Producer Extends, Consumer Super (
? extends Tfor read-heavy operations,? super Tfor write-heavy operations).