Object Oriented Programming (OOP)

Object-Oriented Programming (OOP) is a programming paradigm based on the concept of objects that contain data and methods. It helps in building scalable, reusable, and maintainable software.


📌 Core Concepts

1. Classes and Objects

Example (Java):

class Car {
    String brand;
    int year;

    void drive() {
        System.out.println("Driving " + brand);
    }
}

public class Main {
    public static void main(String[] args) {
        Car car1 = new Car();
        car1.brand = "Toyota";
        car1.year = 2022;
        car1.drive();
    }
}

2. Abstraction

abstract class Animal {
    abstract void sound();
}

class Dog extends Animal {
    void sound() {
        System.out.println("Woof Woof");
    }
}

3. Encapsulation

class BankAccount {
    private double balance;

    public void deposit(double amount) {
        balance += amount;
    }

    public double getBalance() {
        return balance;
    }
}

4. Inheritance

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

class Car extends Vehicle {
    void honk() {
        System.out.println("Car honks");
    }
}

Various types of inheritance are listed below:

  1. Single Inheritance: The single child class inherits characteristics of the single-parent class.
  2. Multiple Inheritance: One class inherits features of more than one base class and is not supported in Java, but the class can implement more than one interface.
  3. Multilevel Inheritance: A class can inherit from a derived class making it a base class for a new class, for example, a Child inherits behavior from his father, and the father has inherited characteristics from his father.
  4. Hierarchical Inheritance: Multiple subclasses inherit one class.
  5. Hybrid Inheritance: This is a combination of single and multiple inheritance.

5. Polymorphism

class Shape {
    void draw() {
        System.out.println("Drawing Shape");
    }
}

class Circle extends Shape {
    void draw() {
        System.out.println("Drawing Circle");
    }
}

6. Composition

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

class Car {
    Engine engine = new Engine();
    void drive() {
        engine.start();
        System.out.println("Car drives");
    }
}

7. Association, Aggregation, and Composition

8. Interfaces

interface Flyable {
    void fly();
}

class Bird implements Flyable {
    public void fly() {
        System.out.println("Bird flies");
    }
}

9. Abstract Classes vs Interfaces

Abstract Classes(partial blueprint + shared code) vs Interfaces(contract, no code, multiple allowed)

Methods

Inheritance

Variables

10. Method Overloading

Definition: Multiple methods in the same class share the same name but differ in parameter lists (type, number, or order). Compile-time polymorphism.

Example (Java):

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

    int add(int a, int b, int c) {
        return a + b + c;
    }

    double add(double a, double b) {
        return a + b;
    }

    public static void main(String[] args) {
        Calculator calc = new Calculator();
        System.out.println(calc.add(2, 3));        // 5
        System.out.println(calc.add(1, 2, 3));     // 6
        System.out.println(calc.add(1.5, 2.5));    // 4.0
    }
}

11. Method Overriding

Definition: A subclass provides a specific implementation of a method that is already defined in its superclass. Runtime polymorphism (dynamic dispatch).

Example (Java):

class Animal {
    void sound() {
        System.out.println("Some generic animal sound");
    }
}

class Dog extends Animal {
    @Override
    void sound() {
        System.out.println("Woof Woof");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal a1 = new Animal();
        Animal a2 = new Dog();   // reference type Animal, runtime object Dog

        a1.sound(); // Some generic animal sound
        a2.sound(); // Woof Woof  (Dog's overridden method called)
    }
}

Notes:

12. Constructor, this, and super (Easy to memorize)

Quick examples (Java):

// Constructor + this example
class Person {
    String name;
    int age;

    // No-arg constructor calls parameterized constructor
    Person() {
        this("Unknown", 0); // calls Person(String,int)
    }

    // Parameterized constructor
    Person(String name, int age) {
        this.name = name; // this disambiguates field vs parameter
        this.age = age;
    }

    void info() {
        System.out.println(name + " - " + age);
    }
}

class Employee extends Person {
    String company;

    // super(...) calls superclass constructor; must be first statement
    Employee(String name, int age, String company) {
        super(name, age); // initialize Person part
        this.company = company;
    }

    void info() {
        super.info(); // call superclass method
        System.out.println("Company: " + company);
    }

    public static void main(String[] args) {
        Person p = new Person("Alice", 30);
        p.info(); // Alice - 30

        Employee e = new Employee("Bob", 25, "Acme");
        e.info();
        // Output:
        // Bob - 25
        // Company: Acme
    }
}

Memorize cheat-sheet:

13. Static vs Dynamic Binding

Example — static binding (overloading):

class OverloadExample {
    void show(int x) { System.out.println("int: " + x); }
    void show(String s) { System.out.println("String: " + s); }

    public static void main(String[] args) {
        OverloadExample o = new OverloadExample();
        o.show(10);       // compile-time selects show(int)
        o.show("hello");  // compile-time selects show(String)
    }
}

Example — dynamic binding (overriding):

class Animal {
    void sound() { System.out.println("Animal sound"); }
}
class Dog extends Animal {
    @Override
    void sound() { System.out.println("Woof"); }
}
public class BindDemo {
    public static void main(String[] args) {
        Animal a = new Dog(); // reference type Animal, object Dog
        a.sound();            // Dog.sound() called at runtime (dynamic binding)
    }
}

Cheat: Overloading = static/compile-time. Overriding = dynamic/runtime.


14. Object Cloning

Shallow clone example:

class Address {
    String city;
    Address(String city) { this.city = city; }
}

class Person implements Cloneable {
    String name;
    Address addr;
    Person(String name, Address addr) { this.name = name; this.addr = addr; }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone(); // shallow copy
    }

    public static void main(String[] args) throws Exception {
        Address a1 = new Address("NY");
        Person p1 = new Person("Alice", a1);
        Person p2 = (Person) p1.clone(); // shallow: p1.addr == p2.addr

        p2.addr.city = "LA";
        System.out.println(p1.addr.city); // prints "LA" — shared reference
    }
}

Deep clone example (clone referenced objects too):

class Address implements Cloneable {
    String city;
    Address(String city) { this.city = city; }
    @Override protected Address clone() throws CloneNotSupportedException { return (Address) super.clone(); }
}

class Person implements Cloneable {
    String name;
    Address addr;
    Person(String name, Address addr) { this.name = name; this.addr = addr; }

    @Override
    protected Person clone() throws CloneNotSupportedException {
        Person copy = (Person) super.clone();
        copy.addr = this.addr.clone(); // deep clone of address
        return copy;
    }
}

Tip: Prefer copy constructors or serialization for complex deep copies.


15. Inner Classes

Types:

Member inner class example:

class Outer {
    int x = 10;
    class Inner {
        void show() { System.out.println("x = " + x); } // can access outer instance
    }
    public static void main(String[] args) {
        Outer o = new Outer();
        Outer.Inner i = o.new Inner();
        i.show(); // x = 10
    }
}

Static nested class example:

class Outer {
    static class Nested {
        static void info() { System.out.println("static nested"); }
    }
    public static void main(String[] args) {
        Outer.Nested.info();
    }
}

Anonymous class example (Runnable):

public class AnonDemo {
    public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("Anonymous Runnable");
            }
        });
        t.start();
    }
}

Memorize:

16. Generics with OOP

Simple generic class example:

class Box<T> {
    private T value;
    Box(T value) { this.value = value; }
    T get() { return value; }
    void set(T value) { this.value = value; }

    public static void main(String[] args) {
        Box<String> b = new Box<>("hello");
        String s = b.get(); // no cast needed
        System.out.println(s);
    }
}

Generic method and bounded type parameter:

class Utils {
    // Generic method
    static <T> void printArray(T[] arr) {
        for (T e : arr) System.out.println(e);
    }

    // Bounded type parameter (only Number or its subclasses)
    static <T extends Number> double sum(T a, T b) {
        return a.doubleValue() + b.doubleValue();
    }
}

Cheat:


17. Dependency Injection (DI)

Simple Java example (constructor injection):

interface MessageService {
    void send(String msg);
}

class EmailService implements MessageService {
    public void send(String msg) { System.out.println("Email: " + msg); }
}

class UserController {
    private final MessageService service;

    // Dependency provided via constructor (injected)
    UserController(MessageService service) {
        this.service = service;
    }

    void notifyUser(String msg) {
        service.send(msg);
    }

    public static void main(String[] args) {
        MessageService svc = new EmailService();                 // wiring
        UserController ctrl = new UserController(svc);          // inject
        ctrl.notifyUser("Welcome!");
    }
}

Notes:


18. UML Diagrams for OOP Design

Class diagram basics:

Example (textual UML for simple model):

Relationships:

Quick tips to draw useful UML:

Cheat:

19. Design Patterns (Quick & Memorize)

Design patterns solve common design problems. Grouped by purpose.

Creational Patterns

// Simple thread-safe lazy singleton
class Singleton {
    private static volatile Singleton instance;
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) instance = new Singleton();
            }
        }
        return instance;
    }
}
interface Shape { void draw(); }
class Circle implements Shape { public void draw(){ System.out.println("Circle"); } }
class Square implements Shape { public void draw(){ System.out.println("Square"); } }

class ShapeFactory {
    static Shape create(String type) {
        switch(type) {
            case "circle": return new Circle();
            case "square": return new Square();
            default: throw new IllegalArgumentException("Unknown");
        }
    }
}
class Person {
    private final String name; private final int age; private final String city;
    private Person(Builder b){ name=b.name; age=b.age; city=b.city; }
    static class Builder {
        String name; int age; String city;
        Builder name(String n){ this.name=n; return this; }
        Builder age(int a){ this.age=a; return this; }
        Builder city(String c){ this.city=c; return this; }
        Person build(){ return new Person(this); }
    }
}
// Usage: Person p = new Person.Builder().name("A").age(30).city("X").build();

Structural Patterns

interface Power { int getVoltage(); }
class USPower { int voltage() { return 120; } }

class USPowerAdapter implements Power {
    private final USPower up = new USPower();
    public int getVoltage(){ return up.voltage(); }
}
interface Component { void show(); }
class Leaf implements Component { private String name; Leaf(String n){name=n;} public void show(){ System.out.println(name); } }
class Composite implements Component {
    private List<Component> children = new ArrayList<>();
    void add(Component c){ children.add(c); }
    public void show(){ for(Component c: children) c.show(); }
}
interface Coffee { double cost(); String desc(); }
class SimpleCoffee implements Coffee { public double cost(){return 2; } public String desc(){return "Coffee";} }
abstract class CoffeeDecorator implements Coffee {
    protected final Coffee c; CoffeeDecorator(Coffee c){this.c=c;}
    public double cost(){ return c.cost(); } public String desc(){ return c.desc(); }
}
class Milk extends CoffeeDecorator { Milk(Coffee c){super(c);} public double cost(){return super.cost()+0.5;} public String desc(){return super.desc()+" + milk";} }

Behavioral Patterns

interface Observer { void update(String msg); }
class Subject {
    private List<Observer> obs = new ArrayList<>();
    void add(Observer o){ obs.add(o); }
    void notifyAllObservers(String m){ for(Observer o: obs) o.update(m); }
}
class ConcreteObserver implements Observer { private String name; ConcreteObserver(String n){name=n;} public void update(String msg){ System.out.println(name + " got: " + msg); } }
interface PaymentStrategy { void pay(int amount); }
class CreditCard implements PaymentStrategy { public void pay(int a){ System.out.println("Paid " + a + " by card"); } }
class PayPal implements PaymentStrategy { public void pay(int a){ System.out.println("Paid " + a + " by PayPal"); } }
class Checkout {
    private PaymentStrategy strategy;
    Checkout(PaymentStrategy s){ this.strategy = s; }
    void doPay(int amt){ strategy.pay(amt); }
}
interface Command { void execute(); }
class Light { void on(){ System.out.println("Light on"); } void off(){ System.out.println("Light off"); } }
class LightOnCommand implements Command {
    private Light light;
    LightOnCommand(Light l){ this.light = l; }
    public void execute(){ light.on(); }
}
class Remote {
    private Command slot;
    void setCommand(Command c){ this.slot = c; }
    void press(){ if(slot!=null) slot.execute(); }
}

Cheat-sheet:

20. SOLID Principles

  1. Single Responsibility Principle (SRP)
    • A class should have one reason to change — one responsibility.
class ReportGenerator {
    String generate() { /* build report data */ return "report"; }
}
class ReportPrinter {
    void print(String r) { System.out.println(r); }
}
  1. Open/Closed Principle (OCP)
    • Software entities should be open for extension, closed for modification.
interface Shape { double area(); }
class Circle implements Shape { double radius; public double area(){ return Math.PI*radius*radius; } }
class Rectangle implements Shape { double w,h; public double area(){ return w*h; } }
// New shapes added by implementing Shape, no change to existing code.
  1. Liskov Substitution Principle (LSP)
    • Subtypes must be substitutable for their base types.
    • Violation example: subclass that throws unsupported operations (e.g., Ostrich extends Bird but cannot fly). Correct by splitting behaviors.
interface FlyingBird { void fly(); }
class Sparrow implements FlyingBird { public void fly(){ System.out.println("fly"); } }
class Ostrich { /* does not implement FlyingBird */ }
  1. Interface Segregation Principle (ISP)
    • Prefer many small, specific interfaces over a large, general one.
interface Work { void doWork(); }
interface Eat { void eat(); }
class Worker implements Work, Eat { public void doWork(){} public void eat(){} }
  1. Dependency Inversion Principle (DIP)
    • High-level modules should not depend on low-level modules; both should depend on abstractions.
interface MessageService { void send(String msg); }
class EmailService implements MessageService { public void send(String m){ System.out.println("Email:"+m); } }
class Notification {
    private final MessageService svc;
    Notification(MessageService svc){ this.svc = svc; } // depends on abstraction
    void notify(String m){ svc.send(m); }
}

Cheat-sheet:

Object-Oriented Programming (OOP)-āĻ āϕ⧇āύ Types āĻĻāϰāĻ•āĻžāϰ?

Type System āϕ⧀ āϜāĻŋāύāĻŋāϏ?

##OOP-āĻ Types āϕ⧇āύ āĻāϤ āϜāϰ⧁āϰāĻŋ

  1. Code-āĻāϰ Safety āĻŦāĻžāĻĄāĻŧāĻžāϝāĻŧ Type system code-āĻ āϭ⧁āϞ āĻšāĻ“āϝāĻŧāĻžāϰ chance āĻ…āύ⧇āĻ•āϟāĻžāχ āĻ•āĻŽāĻŋāϝāĻŧ⧇ āĻĻ⧇āϝāĻŧāĨ¤ āϝāĻ–āύ āφāĻĒāύāĻŋ āĻāĻ•āϟāĻž method-āĻ specific type-āĻāϰ parameter pass āĻ•āϰāĻžāϰ āĻ•āĻĨāĻž āĻŦāϞ⧇āύ, āϤāĻ–āύ compiler āύāĻŋāĻļā§āϚāĻŋāϤ āĻ•āϰ⧇ āϝ⧇ āĻ āĻŋāĻ• āϏ⧇āχ type-āĻāϰ data-āχ āĻĒāĻžāĻ āĻžāύ⧋ āĻšāĻšā§āϛ⧇āĨ¤
public class Calculator {
    public int add(int a, int b) {
        return a + b;
    }
}
  1. Polymorphism āφāϰ Inheritance OOP-āĻāϰ āĻŽā§‚āϞ concepts āϝ⧇āĻŽāύ polymorphism āφāϰ inheritance — āĻāϗ⧁āϞ⧋ type system-āĻāϰ āωāĻĒāϰ depend āĻ•āϰ⧇āĨ¤ Superclass āφāϰ subclass-āĻāϰ āĻŽāĻ§ā§āϝ⧇ relationship āϤ⧈āϰāĻŋ āĻ•āϰāϤ⧇ types āĻ…āĻŦāĻļā§āϝāχ āϞāĻžāĻ—āĻŦ⧇āĨ¤
class Animal:
    def speak(self):
        pass

class Dog(Animal):
    def speak(self):
        return "āĻ˜ā§‡āω āĻ˜ā§‡āω"

class Cat(Animal):
    def speak(self):
        return "āĻŽāĻŋāω āĻŽāĻŋāω"
  1. Interface āφāϰ Abstraction Type system interface āĻŦāĻžāύāĻžāϤ⧇ help āĻ•āϰ⧇, āϝ⧇āϟāĻž different classes-āĻāϰ āĻŽāĻ§ā§āϝ⧇ āĻāĻ•āϟāĻž contract āϤ⧈āϰāĻŋ āĻ•āϰ⧇āĨ¤ āĻāϤ⧇ code āφāϰ⧋ organized āφāϰ maintainable āĻšāϝāĻŧ⧇ āϝāĻžāϝāĻŧāĨ¤

  2. IDE Support āφāϰ Autocomplete āϝāĻĻāĻŋ strong type system āĻĨāĻžāϕ⧇ āϤāĻžāĻšāϞ⧇ modern IDEs āĻ…āύ⧇āĻ• better autocomplete, refactoring āφāϰ error detection āĻĻāĻŋāϤ⧇ āĻĒāĻžāϰ⧇āĨ¤ āĻāϤ⧇ developer-āĻĻ⧇āϰ productivity multifold āĻŦ⧇āĻĄāĻŧ⧇ āϝāĻžāϝāĻŧāĨ¤

  3. Documentation āφāϰ Readability Type declarations code-āĻāϰ āĻāĻ•āϧāϰāύ⧇āϰ self-documentation āĻšāĻŋāϏ⧇āĻŦ⧇ āĻ•āĻžāϜ āĻ•āϰ⧇āĨ¤ āĻ…āĻ¨ā§āϝ developers āϝāĻ–āύ code āĻĒāĻĄāĻŧ⧇, āϤāĻ–āύ āϏāĻšāĻœā§‡āχ āĻŦ⧁āĻāϤ⧇ āĻĒāĻžāϰ⧇ āϕ⧋āύ method āϕ⧀ āϧāϰāύ⧇āϰ data āύ⧇āϝāĻŧ āφāϰ āϕ⧀ return āĻ•āϰ⧇āĨ¤

Static vs Dynamic Typing

OOP languages āĻĻ⧁āχ āĻ­āĻžāϗ⧇ āĻĒāĻĄāĻŧ⧇:

Static Typing (Java, C++, C#): Compile time-āĻ type check āĻšāϝāĻŧāĨ¤ āĻŦ⧇āĻļāĻŋ safe āĻ•āĻŋāĻ¨ā§āϤ⧁ āĻāĻ•āϟ⧁ verbose āϞāĻžāϗ⧇āĨ¤

Dynamic Typing (Python, Ruby, JavaScript): Runtime-āĻ type check āĻšāϝāĻŧāĨ¤ āĻŦ⧇āĻļāĻŋ flexible āĻ•āĻŋāĻ¨ā§āϤ⧁ runtime errors-āĻāϰ risk āĻĨāĻžāϕ⧇āĨ¤

Modern Solution: Gradual Typing

āφāϧ⧁āύāĻŋāĻ• languages āϝ⧇āĻŽāύ TypeScript āφāϰ Python (type hints āϏāĻš) gradual typing support āĻ•āϰ⧇āĨ¤ āĻāϟāĻž dynamic language-āĻāϰ flexibility āφāϰ static language-āĻāϰ safety āĻĻ⧁āĻŸā§‹āχ āĻĻ⧇āϝāĻŧāĨ¤

class Person {
    constructor(public name: string, public age: number) {}

    greet(): string {
        return `āφāĻŽāĻžāϰ āύāĻžāĻŽ ${this.name}, āφāĻŽāĻžāϰ āĻŦāϝāĻŧāϏ ${this.age}`;
    }
}

difference between extends and implements?

  1. Extends: A class can extend another class (child extending parent by inheriting his characteristics). Interface as well inherit (using keyword extends) another interface. Implements: A class can implement an interface

  2. Extends:Sub class extending super class may not override all of the super class methods Implements:Class implementing interface has to implement all the methods of the interface.

  3. Extends:Class can only extend a single super class. Implements: Class can implement any number of interfaces.

different access modifiers:

  1. Default access modifier is without any access specifier data members, class and methods, and are accessible within the same package.
  2. Private access modifiers are marked with the keyword private, and are accessible only within class, and not even accessible by class from the same package.
  3. Protected access modifiers can be accessible within the same package or subclasses from different packages.
  4. Public access modifiers are accessible from everywhere.

constructor

difference between Runtime and compile-time polymorphism

  1. Compile Time Polymorphism: Call is resolved by a compiler in compile-time polymorphism. Runtime Polymorphism: Call is not resolved by the compiler in runtime polymorphism.

  2. Compile Time Polymorphism:It is also known as static binding and method overloading. Runtime Polymorphism: It is also known as dynamic, late, and method overriding.

  3. Compile Time Polymorphism: Same name methods with different parameters or methods with the same signature and different return types are compile-time polymorphism. Runtime Polymorphism: Same name method with the same parameters or signature associated in different classes are called method overriding.