Full Stack • Java • System Design • Cloud • AI Engineering

Java2026-06-11

Complete OOP Guide: Principles, Relationships & Real-World Examples

Master Object-Oriented Programming with detailed explanations of IS-A, HAS-A, USES-A relationships, Aggregation, Composition, Associations, and SOLID principles with real-world examples and diagrams.

Complete Object-Oriented Programming (OOP) Guide

📋 Table of Contents

  1. Introduction to OOP
  2. Core OOP Concepts
  3. OOP Relationships
  4. Real-World Examples
  5. SOLID Principles
  6. Best Practices
  7. Interview Questions

Introduction to OOP

Object-Oriented Programming (OOP) is a programming paradigm that organizes software design around objects rather than functions and logic.

Why OOP?

Benefits:

  • Code Reusability
  • Modularity
  • Flexibility
  • Maintainability
  • Security through Encapsulation

Core OOP Concepts

1. Class and Object

Class: The Blueprint

public class BankAccount {
    private String accountNumber;
    private double balance;
    
    public BankAccount(String accountNumber) {
        this.accountNumber = accountNumber;
        this.balance = 0.0;
    }
    
    public void deposit(double amount) {
        balance += amount;
    }
    
    public double getBalance() {
        return balance;
    }
}

Object: The Instance

BankAccount account1 = new BankAccount("ACC001");
BankAccount account2 = new BankAccount("ACC002");

2. Encapsulation

Definition: Bundling data and methods together, restricting direct access.

public class Employee {
    private String name;
    private double salary;
    
    public void setSalary(double salary) {
        if (salary > 0) {
            this.salary = salary;
        }
    }
    
    public double getSalary() {
        return salary;
    }
}

3. Inheritance (IS-A Relationship)

Definition: Child class inherits properties from parent class.

public class Vehicle {
    protected String brand;
    
    public void start() {
        System.out.println("Vehicle starting");
    }
}

public class Car extends Vehicle {
    private int doors;
    
    public void honk() {
        System.out.println("Beep beep!");
    }
}

4. Polymorphism

Method Overloading (Compile-time)

public class Calculator {
    public int add(int a, int b) {
        return a + b;
    }
    
    public double add(double a, double b) {
        return a + b;
    }
}

Method Overriding (Runtime)

public class Payment {
    public void process() {
        System.out.println("Processing payment");
    }
}

public class CreditCardPayment extends Payment {
    @Override
    public void process() {
        System.out.println("Processing credit card payment");
    }
}

5. Abstraction

Definition: Hiding implementation details, showing only essential features.

public abstract class Shape {
    abstract double calculateArea();
    
    public void display() {
        System.out.println("Area: " + calculateArea());
    }
}

public class Circle extends Shape {
    private double radius;
    
    @Override
    double calculateArea() {
        return Math.PI * radius * radius;
    }
}

OOP Relationships

1. IS-A Relationship (Inheritance)

Example: Dog IS-A Animal

public class Animal {
    public void eat() {
        System.out.println("Eating");
    }
}

public class Dog extends Animal {
    public void bark() {
        System.out.println("Barking");
    }
}

Diagram:

    Animal
      │
      ├── Dog
      ├── Cat
      └── Bird

2. HAS-A Relationship

Composition (Strong)

Example: Car HAS-A Engine

public class Engine {
    public void start() {
        System.out.println("Engine started");
    }
}

public class Car {
    private Engine engine;
    
    public Car() {
        this.engine = new Engine(); // Created with Car
    }
}

Aggregation (Weak)

Example: Department HAS-A Student

public class Student {
    private String name;
}

public class Department {
    private List<Student> students; // Students exist independently
    
    public void addStudent(Student student) {
        students.add(student);
    }
}

Diagram:

Composition:
┌─────┐
│ Car │──■ Engine (dies with Car)
└─────┘

Aggregation:
┌────────────┐
│ Department │◇─── Student (exists independently)
└────────────┘

3. USES-A Relationship (Dependency)

Example: Chef USES-A Recipe

public class Recipe {
    private String name;
}

public class Chef {
    public void cook(Recipe recipe) {
        System.out.println("Cooking " + recipe.getName());
    }
}

4. Association

Example: Doctor ↔ Patient (Many-to-Many)

public class Doctor {
    private List<Patient> patients;
    
    public void assignPatient(Patient patient) {
        patients.add(patient);
    }
}

public class Patient {
    private List<Doctor> doctors;
    
    public void assignDoctor(Doctor doctor) {
        doctors.add(doctor);
    }
}

5. Generalization

Definition: Extracting common features to create a parent class.

// Generalized from FullTimeEmployee, PartTimeEmployee, Contractor
public abstract class Employee {
    protected String name;
    protected double salary;
    
    public abstract double calculateSalary();
}

Real-World Examples

Example 1: E-Commerce System

// Product hierarchy
public abstract class Product {
    protected String id;
    protected String name;
    protected double price;
    
    public abstract void displayDetails();
}

public class Electronics extends Product {
    private int warrantyMonths;
    
    @Override
    public void displayDetails() {
        System.out.println(name + " - $" + price);
        System.out.println("Warranty: " + warrantyMonths + " months");
    }
}

public class Clothing extends Product {
    private String size;
    
    @Override
    public void displayDetails() {
        System.out.println(name + " - $" + price);
        System.out.println("Size: " + size);
    }
}

// Shopping Cart (Aggregation)
public class ShoppingCart {
    private List<Product> products;
    
    public void addProduct(Product product) {
        products.add(product);
    }
    
    public double calculateTotal() {
        return products.stream()
            .mapToDouble(p -> p.price)
            .sum();
    }
}

// Order (Composition)
public class Order {
    private String orderId;
    private ShoppingCart cart;
    private Payment payment;
    
    public Order(String orderId) {
        this.orderId = orderId;
        this.cart = new ShoppingCart();
    }
    
    public void processOrder(Payment payment) {
        payment.process(cart.calculateTotal());
    }
}

Example 2: Library Management System

// Book (Entity)
public class Book {
    private String isbn;
    private String title;
    private String author;
    private boolean isAvailable;
    
    public void checkOut() {
        isAvailable = false;
    }
    
    public void returnBook() {
        isAvailable = true;
    }
}

// Member (Entity)
public class Member {
    private String memberId;
    private String name;
    private List<Book> borrowedBooks;
    
    public void borrowBook(Book book) {
        if (book.isAvailable()) {
            borrowedBooks.add(book);
            book.checkOut();
        }
    }
}

// Library (Aggregation)
public class Library {
    private List<Book> books;
    private List<Member> members;
    
    public void addBook(Book book) {
        books.add(book);
    }
    
    public void registerMember(Member member) {
        members.add(member);
    }
}

Example 3: Hospital Management System

// Person (Generalization)
public abstract class Person {
    protected String id;
    protected String name;
    protected int age;
    
    public abstract void displayInfo();
}

// Doctor IS-A Person
public class Doctor extends Person {
    private String specialization;
    private List<Patient> patients;
    
    @Override
    public void displayInfo() {
        System.out.println("Dr. " + name + " - " + specialization);
    }
    
    public void diagnose(Patient patient) {
        System.out.println("Diagnosing " + patient.getName());
    }
}

// Patient IS-A Person
public class Patient extends Person {
    private String condition;
    private List<Doctor> doctors;
    
    @Override
    public void displayInfo() {
        System.out.println("Patient: " + name + " - " + condition);
    }
}

// Appointment (Association)
public class Appointment {
    private Doctor doctor;
    private Patient patient;
    private LocalDateTime dateTime;
    
    public void schedule(Doctor doctor, Patient patient, LocalDateTime dateTime) {
        this.doctor = doctor;
        this.patient = patient;
        this.dateTime = dateTime;
    }
}

SOLID Principles

SOLID is an acronym for five design principles that make software designs more understandable, flexible, and maintainable.


1. Single Responsibility Principle (SRP)

Definition: A class should have only ONE reason to change, meaning it should have only ONE job or responsibility.

Why It Matters:

  • ✅ Easier to understand and maintain
  • ✅ Reduces coupling between different parts
  • ✅ Makes testing simpler
  • ✅ Changes in one responsibility don't affect others

Real-World Example: User Management System

Bad - Violates SRP:

// This class has TOO MANY responsibilities!
public class User {
    private String username;
    private String email;
    
    // Responsibility 1: Data management
    public void setUsername(String username) { this.username = username; }
    
    // Responsibility 2: Validation
    public boolean validateEmail(String email) {
        return email.contains("@") && email.contains(".");
    }
    
    // Responsibility 3: Database operations
    public void saveToDatabase() {
        // SQL code here
    }
    
    // Responsibility 4: Email sending
    public void sendWelcomeEmail() {
        // Email sending code
    }
    
    // Responsibility 5: Report generation
    public String generateReport() {
        return "User Report: " + username;
    }
}

Good - Follows SRP:

// 1. User entity - Only manages user data
public class User {
    private String username;
    private String email;
    
    public User(String username, String email) {
        this.username = username;
        this.email = email;
    }
    
    public String getUsername() { return username; }
    public String getEmail() { return email; }
}

// 2. Email validator - Only validates emails
public class EmailValidator {
    public boolean validate(String email) {
        return email != null && 
               email.contains("@") && 
               email.contains(".");
    }
}

// 3. User repository - Only handles database
public class UserRepository {
    public void save(User user) {
        // Database save logic
    }
    
    public User findByUsername(String username) {
        // Database query logic
        return null;
    }
}

// 4. Email service - Only sends emails
public class EmailService {
    public void sendWelcomeEmail(User user) {
        String message = "Welcome " + user.getUsername();
        // Send email logic
    }
}

// 5. Report generator - Only generates reports
public class UserReportGenerator {
    public String generate(User user) {
        return "User Report: " + user.getUsername();
    }
}

Benefits:

Benefit Description
Maintainability Email changes don't affect database code
Testability Can test EmailValidator independently
Reusability EmailService can be used elsewhere
Clarity Each class has one clear purpose

2. Open/Closed Principle (OCP)

Definition: Software entities should be OPEN for extension but CLOSED for modification.

Why It Matters:

  • ✅ Add new functionality without changing existing code
  • ✅ Reduces risk of breaking existing features
  • ✅ Promotes code stability
  • ✅ Enables plugin architectures

Real-World Example: Payment Processing

Bad - Requires Modification:

// Must MODIFY this class for every new payment method
public class PaymentProcessor {
    public void process(String type, double amount) {
        if (type.equals("CREDIT_CARD")) {
            System.out.println("Processing credit card: $" + amount);
            // Credit card logic
        } else if (type.equals("PAYPAL")) {
            System.out.println("Processing PayPal: $" + amount);
            // PayPal logic
        } else if (type.equals("BITCOIN")) {
            // Need to MODIFY class again!
            System.out.println("Processing Bitcoin: $" + amount);
        }
    }
}

Good - Extension Without Modification:

// 1. Define abstraction
public interface PaymentMethod {
    void processPayment(double amount);
    String getName();
}

// 2. Implement concrete methods
public class CreditCardPayment implements PaymentMethod {
    private String cardNumber;
    
    @Override
    public void processPayment(double amount) {
        System.out.println("Processing Credit Card: $" + amount);
        validateCard();
        contactGateway();
    }
    
    @Override
    public String getName() { return "Credit Card"; }
    
    private void validateCard() { /* validation */ }
    private void contactGateway() { /* gateway call */ }
}

public class PayPalPayment implements PaymentMethod {
    private String email;
    
    @Override
    public void processPayment(double amount) {
        System.out.println("Processing PayPal: $" + amount);
        redirectToPayPal();
    }
    
    @Override
    public String getName() { return "PayPal"; }
    
    private void redirectToPayPal() { /* redirect */ }
}

// NEW payment method - NO modification needed!
public class BitcoinPayment implements PaymentMethod {
    @Override
    public void processPayment(double amount) {
        System.out.println("Processing Bitcoin: $" + amount);
        generateWallet();
    }
    
    @Override
    public String getName() { return "Bitcoin"; }
    
    private void generateWallet() { /* wallet */ }
}

// 3. Processor - CLOSED for modification
public class PaymentProcessor {
    public void process(PaymentMethod method, double amount) {
        System.out.println("Using: " + method.getName());
        method.processPayment(amount);
    }
}

Benefits:

Benefit Description
Stability Existing code remains untouched
Extensibility Easy to add new payment methods
Testing Existing tests don't need changes
Plugin Support Dynamic loading of implementations

3. Liskov Substitution Principle (LSP)

Definition: Objects of a superclass should be replaceable with objects of its subclasses without breaking the application.

Why It Matters:

  • ✅ Ensures inheritance is used correctly
  • ✅ Prevents unexpected behavior
  • ✅ Maintains contract consistency
  • ✅ Enables safe polymorphism

Real-World Example: Shape Hierarchy

Bad - Violates LSP:

public class Rectangle {
    protected int width;
    protected int height;
    
    public void setWidth(int width) { this.width = width; }
    public void setHeight(int height) { this.height = height; }
    public int getArea() { return width * height; }
}

// Square violates LSP!
public class Square extends Rectangle {
    @Override
    public void setWidth(int width) {
        this.width = width;
        this.height = width; // Forces equal sides
    }
    
    @Override
    public void setHeight(int height) {
        this.width = height;  // Forces equal sides
        this.height = height;
    }
}

// This breaks!
Rectangle rect = new Square();
rect.setWidth(5);
rect.setHeight(10);
System.out.println(rect.getArea()); // Expected 50, got 100!

Good - Follows LSP:

// 1. Create proper abstraction
public interface Shape {
    double calculateArea();
    String getType();
}

// 2. Independent implementations
public class Rectangle implements Shape {
    private double width;
    private double height;
    
    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }
    
    @Override
    public double calculateArea() {
        return width * height;
    }
    
    @Override
    public String getType() { return "Rectangle"; }
}

public class Square implements Shape {
    private double side;
    
    public Square(double side) {
        this.side = side;
    }
    
    @Override
    public double calculateArea() {
        return side * side;
    }
    
    @Override
    public String getType() { return "Square"; }
}

// Works correctly!
Shape rect = new Rectangle(5, 10);
Shape square = new Square(5);
System.out.println(rect.calculateArea());   // 50 ✓
System.out.println(square.calculateArea()); // 25 ✓

Benefits:

Benefit Description
Predictability Subclasses behave as expected
Reliability No surprises when substituting
Polymorphism Safe to use base references
Maintainability Clear contracts

4. Interface Segregation Principle (ISP)

Definition: Clients should not be forced to depend on interfaces they don't use.

Why It Matters:

  • ✅ Prevents fat interfaces
  • ✅ Reduces coupling
  • ✅ Improves flexibility
  • ✅ Makes code maintainable

Real-World Example: Worker System

Bad - Fat Interface:

// Forces all workers to implement everything!
public interface Worker {
    void work();
    void eat();
    void sleep();
    void attendMeeting();
    void submitTimesheet();
}

public class HumanWorker implements Worker {
    // Can implement all
    public void work() { }
    public void eat() { }
    public void sleep() { }
    public void attendMeeting() { }
    public void submitTimesheet() { }
}

public class RobotWorker implements Worker {
    public void work() { }
    
    // Forced to implement methods it doesn't need!
    public void eat() { 
        throw new UnsupportedOperationException();
    }
    public void sleep() { 
        throw new UnsupportedOperationException();
    }
    public void attendMeeting() { 
        throw new UnsupportedOperationException();
    }
    public void submitTimesheet() { 
        throw new UnsupportedOperationException();
    }
}

Good - Segregated Interfaces:

// 1. Focused interfaces
public interface Workable {
    void work();
}

public interface Eatable {
    void eat();
}

public interface Sleepable {
    void sleep();
}

public interface Meetable {
    void attendMeeting();
}

// 2. Implement only what's needed
public class HumanWorker implements Workable, Eatable, Sleepable, Meetable {
    @Override
    public void work() {
        System.out.println("Human working");
    }
    
    @Override
    public void eat() {
        System.out.println("Human eating");
    }
    
    @Override
    public void sleep() {
        System.out.println("Human sleeping");
    }
    
    @Override
    public void attendMeeting() {
        System.out.println("Human in meeting");
    }
}

public class RobotWorker implements Workable {
    @Override
    public void work() {
        System.out.println("Robot working 24/7");
    }
    
    // No need to implement eat, sleep, attendMeeting!
}

public class Manager implements Workable, Meetable {
    @Override
    public void work() {
        System.out.println("Manager managing");
    }
    
    @Override
    public void attendMeeting() {
        System.out.println("Manager leading meeting");
    }
}

Benefits:

Benefit Description
Flexibility Classes implement only what they need
Maintainability Changes to one interface don't affect others
Clarity Clear, focused interfaces
Reusability Small interfaces are more reusable

5. Dependency Inversion Principle (DIP)

Definition:

  1. High-level modules should not depend on low-level modules. Both should depend on abstractions.
  2. Abstractions should not depend on details. Details should depend on abstractions.

Why It Matters:

  • ✅ Reduces coupling
  • ✅ Increases flexibility
  • ✅ Enables dependency injection
  • ✅ Makes testing easier

Real-World Example: Notification System

Bad - High-Level Depends on Low-Level:

// Low-level module
public class EmailService {
    public void sendEmail(String to, String message) {
        System.out.println("Sending email to: " + to);
    }
}

// High-level module DIRECTLY depends on low-level
public class UserService {
    private EmailService emailService = new EmailService(); // Tight coupling!
    
    public void registerUser(String username, String email) {
        // Registration logic
        emailService.sendEmail(email, "Welcome!");
        // Cannot switch to SMS without modifying this class!
    }
}

Good - Depend on Abstractions:

// 1. Define abstraction
public interface NotificationService {
    void send(String recipient, String message);
    String getType();
}

// 2. Low-level implementations
public class EmailService implements NotificationService {
    @Override
    public void send(String recipient, String message) {
        System.out.println("Email to " + recipient + ": " + message);
    }
    
    @Override
    public String getType() { return "Email"; }
}

public class SMSService implements NotificationService {
    @Override
    public void send(String recipient, String message) {
        System.out.println("SMS to " + recipient + ": " + message);
    }
    
    @Override
    public String getType() { return "SMS"; }
}

public class PushNotificationService implements NotificationService {
    @Override
    public void send(String recipient, String message) {
        System.out.println("Push to " + recipient + ": " + message);
    }
    
    @Override
    public String getType() { return "Push"; }
}

// 3. High-level depends on abstraction (Dependency Injection)
public class UserService {
    private NotificationService notificationService;
    
    // Dependency injected via constructor
    public UserService(NotificationService notificationService) {
        this.notificationService = notificationService;
    }
    
    public void registerUser(String username, String email) {
        // Registration logic
        System.out.println("Registering: " + username);
        notificationService.send(email, "Welcome " + username + "!");
        System.out.println("Used: " + notificationService.getType());
    }
}

// 4. Usage - Easy to switch implementations!
public class Application {
    public static void main(String[] args) {
        // Use Email
        UserService service1 = new UserService(new EmailService());
        service1.registerUser("john", "[email protected]");
        
        // Switch to SMS - NO code changes in UserService!
        UserService service2 = new UserService(new SMSService());
        service2.registerUser("jane", "+1234567890");
        
        // Switch to Push - NO code changes in UserService!
        UserService service3 = new UserService(new PushNotificationService());
        service3.registerUser("bob", "device-token-123");
    }
}

Benefits:

Benefit Description
Flexibility Easy to swap implementations
Testability Can inject mock objects for testing
Maintainability Changes isolated to implementations
Decoupling High-level code independent of details

SOLID Principles Summary

Principle Key Point Benefit
SRP One class, one responsibility Easy to maintain
OCP Open for extension, closed for modification Stable codebase
LSP Subtypes must be substitutable Safe polymorphism
ISP No fat interfaces Flexible design
DIP Depend on abstractions Loose coupling

When to Apply SOLID:

  • ✅ Designing new systems
  • ✅ Refactoring existing code
  • ✅ Building scalable applications
  • ✅ Creating reusable libraries

Remember: SOLID principles are guidelines, not strict rules. Apply them judiciously based on your specific needs!



Best Practices

1. Favor Composition Over Inheritance

// Instead of inheritance
public class Bird {
    public void fly() { }
}

public class Penguin extends Bird {
    // Problem: Penguins can't fly!
}

// Use composition
public interface Flyable {
    void fly();
}

public class Bird {
    private Flyable flyBehavior;
    
    public void performFly() {
        if (flyBehavior != null) {
            flyBehavior.fly();
        }
    }
}

2. Program to Interface, Not Implementation

// Good
List<String> names = new ArrayList<>();

// Instead of
ArrayList<String> names = new ArrayList<>();

3. Keep Classes Focused

// Each class has one clear purpose
public class User { }
public class UserValidator { }
public class UserRepository { }
public class UserService { }

Interview Questions

Q1: What are the four pillars of OOP?

Answer:

  1. Encapsulation - Data hiding
  2. Inheritance - Code reuse
  3. Polymorphism - Many forms
  4. Abstraction - Hide complexity

Q2: Difference between Composition and Aggregation?

Answer:

Aspect Composition Aggregation
Relationship Strong HAS-A Weak HAS-A
Lifetime Dependent Independent
Example Car-Engine Department-Student

Q3: What is the difference between IS-A and HAS-A?

Answer:

  • IS-A (Inheritance): Dog IS-A Animal
  • HAS-A (Composition/Aggregation): Car HAS-A Engine

Q4: Explain method overloading vs overriding?

Answer:

Overloading (Compile-time):

public int add(int a, int b) { }
public double add(double a, double b) { }

Overriding (Runtime):

class Parent {
    void display() { }
}
class Child extends Parent {
    @Override
    void display() { }
}

Q5: What is the Diamond Problem?

Answer: The diamond problem occurs in multiple inheritance when a class inherits from two classes that have a common parent.

Java solves this by:

  • Not allowing multiple class inheritance
  • Allowing multiple interface implementation
  • Using default methods in interfaces (Java 8+)

Q6: Explain SOLID principles briefly?

Answer:

  • S - Single Responsibility
  • O - Open/Closed
  • L - Liskov Substitution
  • I - Interface Segregation
  • D - Dependency Inversion

Q7: When to use abstract class vs interface?

Answer:

Abstract Class:

  • Common code to share
  • Related classes
  • Non-public members needed

Interface:

  • Unrelated classes
  • Multiple inheritance needed
  • Define contract only

Q8: What is tight coupling vs loose coupling?

Answer:

Tight Coupling:

public class A {
    private B b = new B(); // Tightly coupled
}

Loose Coupling:

public class A {
    private IB b; // Loosely coupled through interface
    
    public A(IB b) {
        this.b = b;
    }
}

Q9: Explain Association, Aggregation, and Composition?

Answer:

Association: General relationship

Doctor ↔ Patient

Aggregation: Weak HAS-A (independent lifecycle)

Department ◇─ Student

Composition: Strong HAS-A (dependent lifecycle)

Car ──■ Engine

Q10: What is the purpose of the final keyword?

Answer:

  • final variable: Cannot be reassigned
  • final method: Cannot be overridden
  • final class: Cannot be inherited
public final class ImmutableClass {
    private final int value;
    
    public ImmutableClass(int value) {
        this.value = value;
    }
}

Summary

Key Takeaways

  1. OOP organizes code around objects
  2. Four pillars: Encapsulation, Inheritance, Polymorphism, Abstraction
  3. Relationships: IS-A, HAS-A, USES-A, Association
  4. SOLID principles guide good design
  5. Favor composition over inheritance
  6. Program to interfaces

Relationship Summary

Relationship Type Example Diagram
IS-A Inheritance Dog IS-A Animal Parent → Child
HAS-A (Strong) Composition Car HAS-A Engine Container ──■ Part
HAS-A (Weak) Aggregation Dept HAS-A Student Container ◇─ Part
USES-A Dependency Chef USES-A Recipe User ----→ Used
Association Relationship Doctor ↔ Patient A ◆────◆ B

Next Steps:

  • Practice with real-world projects
  • Study design patterns
  • Review SOLID principles
  • Build portfolio projects