Structured vs Object Oriented Programming #


πŸ“Œ 1: Structured Programming

  • A procedural approach that focuses on functions (procedures) to break down a problem into steps.
  • Control flow is managed using function calls.
  • Example: Taking a Flight (Structured Programming)
    #include <stdio.h>
    
    void travelToAirport() { 
        printf("Traveling to the airport...\n"); 
    }
    
    void checkIn() { 
        printf("Checking in...\n"); 
    }
    
    void boardFlight() { 
        printf("Boarding the flight...\n"); 
    }
    
    int main() {
        travelToAirport();
        checkIn();
        boardFlight();
        return 0;
    }

πŸ“Œ 2: Object-Oriented Programming (OOP)

  • Focuses on objects rather than procedures.
  • Objects combine data (state) and methods (behavior).
  • Encapsulation, inheritance, and polymorphism make code reusable and modular.
  • Example: Taking a Flight (OOP Approach)
    // Main class to execute the program
    public class FlightProcess {
        public static void main(String[] args) {
            
            Passenger passenger1 = new Passenger("Alice");
    
            Car taxiCar = new Car("Toyota Camry");
    
            TaxiService taxiService = new TaxiService();
            
            taxiService.driveToAirport(passenger1, taxiCar);
            
            AirportCheckInDesk checkInDesk 
                        = new AirportCheckInDesk();
            
            Flight flight = new Flight("AI-202");
            
            checkInDesk.checkIn(passenger1, flight);
            
            passenger1.boardFlight(flight);
        }
    }
    
    
    // Represents a Car that a taxi service uses
    class Car {
        private String model;
    
        Car(String model) {
            this.model = model;
        }
    
        public String getModel() {
            return model;
        }
    }
    
    // Represents a Flight
    class Flight {
        private String flightNumber;
    
        Flight(String flightNumber) {
            this.flightNumber = flightNumber;
        }
    
        public String getFlightNumber() {
            return flightNumber;
        }
    }
    
    // Represents a Passenger
    class Passenger {
        private String name;
    
        Passenger(String name) {
            this.name = name;
        }
    
        public String getName() {
            return name;
        }
    
        // Passenger now initiates boarding
        void boardFlight(Flight flight) {
            System.out.println(name + 
                " is boarding flight " + 
                flight.getFlightNumber());
        }
    }
    
    // Handles taxi services
    class TaxiService {
        void driveToAirport(Passenger passenger, Car car) {
            System.out.println(passenger.getName() 
                + " is traveling to the airport in a " 
                + car.getModel());
        }
    }
    
    // Handles check-in at the airport
    class AirportCheckInDesk {
        void checkIn(Passenger passenger, Flight flight) {
            System.out.println(passenger.getName() + 
                " is checking in to the flight" + 
                flight.getFlightNumber());
        }
    }

πŸ“Œ 3: Key Differences

Feature Structured Programming Object-Oriented Programming (OOP)
Focus Functions (procedures) Objects (data + methods)
Data Handling Stored separately, functions operate on it Encapsulated within objects
Reusability Code is reused via functions Code is reused via classes and objects
Scalability Harder to scale for large apps Easy to extend and maintain
Example Languages C, Pascal, .. Java, C++, ..

Class vs Object vs State vs Behavior #


πŸ“Œ What is a Class?

  • A blueprint or template that defines what an object should be.

  • Specifies attributes (state) and methods (behavior).

    public class CricketScorer {
        // State (instance variable)
        private int score;
    
        // Behavior (methods)
        public void addFour() {
            score += 4; // Increment score by 4
        }
    
        public void addSix() {
            score += 6; // Increment score by 6
        }
    
        public int getScore() {
            return score; // Return current score
        }
    }

πŸ“Œ What is an Object?

  • An instance of a class, created using the new keyword.
    public class Match {
        public static void main(String[] args) {
            CricketScorer scorer1 
                = new CricketScorer(); // Object 1
            CricketScorer scorer2 
                = new CricketScorer(); // Object 2
            
            //Objects `scorer1` and `scorer2` are instances 
            //with their own scores.
        }
    }

πŸ“Œ What is State?

  • The data stored inside an object (attributes or instance variables).
  • Each object can have different values for its state.
  • State of an object changes with time.
    public class Match {
        public static void main(String[] args) {
            CricketScorer scorer1 = new CricketScorer();
            CricketScorer scorer2 = new CricketScorer();
    
            scorer1.addFour(); // scorer1's score = 4
            scorer2.addSix();  // scorer2's score = 6
    
            System.out.println("Scorer1 Score: " 
                            + scorer1.getScore()); // 4
            System.out.println("Scorer2 Score: " 
                            + scorer2.getScore()); // 6
    
            //Each object maintains its own `score`, 
            //demonstrating unique state.
        }
    }

πŸ“Œ What is Behavior?

  • The actions an object can perform, defined as methods in a class.
    public class Match {
        public static void main(String[] args) {
            CricketScorer scorer = new CricketScorer();
            
            // Behavior: Increase score by 4
            scorer.addFour(); 
            
            // Behavior: Increase score by 6
            scorer.addSix();  
    
            System.out.println("Total Score: " 
                        + scorer.getScore()); // 10
        }
    }

πŸ“Œ Summary: Class vs Object vs State vs Behavior

Concept Definition Example (CricketScorer)
Class A blueprint defining state & behavior class CricketScorer { int score; void addFour() { ... } }
Object An instance of a class CricketScorer scorer = new CricketScorer();
State Data inside an object score variable (each object has its own score)
Behavior Actions an object performs addFour(), addSix(), getScore()

Object Lifecycle #


Stage Description Example (CricketScorer)
1: Object Creation An object is created using new, calling the constructor CricketScorer scorer = new CricketScorer();
2: Object in Use Methods and fields are accessed scorer.addFour(); scorer.getScore();
3: Object Becomes Unreachable No references point to the object, making it eligible for GC scorer = null;
4: Garbage Collection JVM removes the object from memory when needed System.gc(); (suggests GC)
5: Finalization (Deprecated in Java 9) finalize() runs before the object is destroyed JVM handles this step

Why is Java NOT 100% Object-Oriented? #


100% Object-Oriented Language

  • A 100% object-oriented language is one where everything is an object:
    • Including primitive data types and operations.
  • Example: Smalltalk, Ruby

Why Java is NOT Fully Object-Oriented?

Reason Explanation Example
Primitive Data Types Java has primitive types (int, char, double, etc.), which are not objects. int x = 10; (not an object)
Operators Are Not Methods Operators like +, -, *, and / do not invoke methods, unlike pure OOP languages. int sum = a + b; (direct operation)
Static Methods Methods declared as static belong to the class instead of an object. Math.pow(2,3); (called without an object)
static Variables static variables exist independently of objects. static int count; (shared across all instances)

Inheritance vs Abstract Class vs Interface #


πŸ“Œ Overview

  • Inheritance: Allows a subclass to inherit properties and methods from a parent class.
  • Abstract Class: Defines a common template with both implemented and unimplemented (abstract) methods.
  • Interface: Defines a contract (an interface) that multiple classes can implement.

πŸ“Œ Inheritance

  • What? Allows a subclass to reuse code from a parent class.
  • Why? Reduces duplication and models real-world relationships.
  • How? Uses the extends keyword to inherit from a class.
  • Limitations: Tight coupling – changes in the parent class affect all subclasses.
    public class Animal {
        public void eat() {
            System.out.println("This animal eats food.");
        }
    }
    
    public class Dog extends Animal {
        public void bark() {
            System.out.println("Dog barks.");
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            Dog dog = new Dog();
            dog.eat(); // Inherited from Animal
            dog.bark();
        }
    }

πŸ“Œ Abstract Class

  • What? A partially implemented class that contains both abstract and concrete methods.
  • Why? Provides a common template for related classes while allowing specific implementations.
  • How? Uses the abstract keyword and defines abstract methods for subclasses to implement.
    public abstract class AbstractRecipe {
        public void execute() {
            getReady();
            doTheDish();
            cleanup();
        }
    
        public abstract void getReady();
        public abstract void doTheDish();
        public abstract void cleanup();
    }
    
    public class Recipe1 extends AbstractRecipe {
        @Override
        public void getReady() {
            System.out.println("Get the raw materials");
            System.out.println("Get the utensils");
        }
    
        @Override
        public void doTheDish() {
            System.out.println("Cook the dish");
        }
    
        @Override
        public void cleanup() {
            System.out.println("Cleanup the utensils");
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            AbstractRecipe recipe = new Recipe1();
            recipe.execute();
        }
    }

πŸ“Œ Interface

  • What? A contract that multiple classes can implement without sharing implementation details.
  • Why? Allows multiple classes (even unrelated) to share common behavior.
  • How? Uses the interface keyword and requires implementing classes to define all methods.
    interface Flyable {
        void fly();
    }
    
    class Aeroplane implements Flyable {
        @Override
        public void fly() {
            System.out.println(
                "Aeroplane flies using engines.");
        }
    }
    
    class Bird implements Flyable {
        @Override
        public void fly() {
            System.out.println(
                "Bird flies by flapping wings.");
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            Flyable plane = new Aeroplane();
            plane.fly();
    
            Flyable sparrow = new Bird();
            sparrow.fly();
        }
    }

πŸ“Œ Comparison

Feature Inheritance (extends) Abstract Class Interface (implements)
Definition A subclass inherits methods from a parent class A class with abstract and concrete methods A blueprint with only method signatures
Supports Multiple Inheritance? No No Yes
Contains Abstract Methods? No Yes Yes
Contains Concrete Methods? Yes Yes Yes (From Java 8: default methods)
Can Have Instance Variables? Yes Yes No
Can Have Constructors? Yes Yes No
Best Use Case When a class is a specialized version (IS-A) of another class When multiple subclasses share common behavior but need some customization in specific implementations When multiple classes (even unrelated) need to follow the same contract

πŸ“Œ When to Use What?

  • Use Inheritance β†’ When there is a parent-child relationship (e.g., Animal β†’ Dog).
  • Use Abstract Class β†’ When multiple sub-classes share common behavior but need some customization in specific implementations (e.g., AbstractRecipe β†’ Recipe1).
  • Use Interface β†’ When multiple classes (even unrelated) must follow the same contract (e.g., Flyable β†’ Bird, Aeroplane).

Design Choices in Java Collections: Interface, Inheritance, and Abstract Classes #


πŸ“Œ Overview

  • Java Collections Framework is designed to be flexible, reusable, and extendable.
  • Follows a hierarchical structure using interfaces, abstract classes, and concrete implementations.
    • Interfaces β†’ Define common behavior (List, Set, Queue, ..).
    • Abstract Classes β†’ Provide initial implementations (AbstractList, AbstractSet, ..).
    • Concrete Classes β†’ Implement specific data structures (ArrayList, HashSet, ...).

πŸ“Œ Example: List Interface in JDK

public interface List<E> extends Collection<E> {
    boolean add(E e);
    E get(int index);
    boolean remove(Object o);
    //Others
}

πŸ“Œ Example: AbstractList in JDK

//Provides partial implementation, allowing customization
public abstract class AbstractList<E> 
        extends AbstractCollection<E> 
        implements List<E> {
    
    public boolean add(E e) {
        add(size(), e);  // Default add() method
        return true;
    }

    public E remove(int index) {
        throw new UnsupportedOperationException();  
            // Must be overridden
    }
    //Others
}

πŸ“Œ Example: ArrayList Extending AbstractList

public class ArrayList<E> extends AbstractList<E> 
                implements List<E>, RandomAccess, Cloneable {
    
    private transient Object[] elementData;

    public E get(int index) {
        return (E) elementData[index];
    }

    public boolean add(E e) {
        ensureCapacityInternal(size + 1);
        elementData[size++] = e;
        return true;
    }

}

What are Design Patterns? #


πŸ“Œ What are Design Patterns?

  • What? Reusable solutions for common coding problems.
  • Why? Makes code easier to understand, change, and reuse.
  • Where? Used in big projects, frameworks, and everyday coding.
  • How? By applying proven patterns like Singleton, Strategy, Factory, and Observer.

πŸ“Œ Example 1: Singleton Pattern

  • What? One object shared across the app.
  • When? Need only one instance (e.g., Logger, Config).
  • How? Private constructor + static instance + lazy creation.
    Logger logger1 = Logger.getInstance();
    Logger logger2 = Logger.getInstance();
    System.out.println(logger1 == logger2); // true
  • Key Benefit: Saves memory, consistent behavior.
  • Implementation
    class Logger {
        private static Logger instance;
    
        private Logger() {
            // private constructor to 
            //prevent external instantiation
        }
    
        public static Logger getInstance() {
            if (instance == null) {
                instance = new Logger();
            }
            return instance;
        }
    
        public void log(String message) {
            System.out.println("LOG: " + message);
        }
    }
    
    public class SingletonPattern {
        public static void main(String[] args) {
            Logger logger1 = Logger.getInstance();
            Logger logger2 = Logger.getInstance();
    
            logger1.log("First message");
            logger2.log("Second message");
    
            System.out.println(logger1 == logger2); // true
        }
    }

πŸ“Œ Example 2: Strategy Pattern

  • What? Swap different algorithms at runtime.
  • When? Want to choose behavior dynamically (Bubble vs Quick sort).
  • How? Interface for strategy + pass concrete implementation.
    Sortable sorter = new BubbleSort(); // or QuickSort
    ComplexClass task = new ComplexClass(sorter);
    task.doAComplexThing();
  • Implementation:
    interface Sortable {
        public int[] sort(int[] numbers);
    }
    
    class BubbleSort implements Sortable {
        public int[] sort(int[] numbers) {
            return numbers; //LOGIC GOES HERE
        }
    }
    
    class QuickSort implements Sortable {
        public int[] sort(int[] numbers) {
            return numbers; //LOGIC GOES HERE
        }
    }
    
    class ComplexClass {
        private Sortable sorter;
    
        ComplexClass(Sortable sorter) {
            this.sorter = sorter;
        }
    
        void doAComplexThing() {
            int[] values = null;
    
            // logic...
    
            sorter.sort(values);
    
            // logic...
        }
    }
    
    public class StrategyPattern {
        public static void main(String[] args) {
            ComplexClass complexClassInstance 
                = new ComplexClass(new BubbleSort());
            complexClassInstance.doAComplexThing();
        }
    }

πŸ“Œ Example 3: Factory Method Pattern

  • What? Central place to create objects.
  • When? You want control over object creation!
  • How? Factory method decides what object to return.
    Shape shape = ShapeFactory.getShape("CIRCLE"); // returns Circle
  • Key Benefit: Decouples object creation from usage.
  • Implementation
    class ShapeFactory {
        public static Shape getShape(String type) {
            if (type.equalsIgnoreCase("CIRCLE")) {
                return new Circle();
            } else if (type.equalsIgnoreCase("SQUARE")) {
                return new Square();
            }
            return null;
        }
    }
    
    class Shape {
    }
    
    class Circle extends Shape {
    }
    
    class Square extends Shape {
    }
    
    public class FactoryMethodPattern {
        public static void main(String[] args) {
            Shape circle 
                = ShapeFactory
                    .getShape("CIRCLE");
            
            Shape square 
                = ShapeFactory
                    .getShape("SQUARE");
        }
    }
    

πŸ“Œ Example 4: Observer Pattern

  • What? One-to-many notification system.
  • When? Need listeners (e.g., UI buttons, events).
  • How? Register listeners, notify them when something happens.
    Listener listener 
        = () -> System.out.println(
                    "Button clicked!");
    
    button.setListener(listener);
    
    button.click();
  • Implementation:
    interface Listener {
        void onClick();
    }
    
    class Button {
        private Listener listener;
    
        public void setListener(Listener listener) {
            this.listener = listener;
        }
    
        public void click() {
            if (listener != null) {
                listener.onClick();
            }
        }
    }
    
    public class ObserverPattern {
        public static void main(String[] args) {
            Button button = new Button();
    
            Listener listener 
                = () -> System.out.println(
                            "Button clicked!");
    
            button.setListener(listener);
    
            button.click();
        }
    }

What is Dependency Injection? #


πŸ“Œ What is Dependency Injection (DI)?

  • What? Passing dependencies from outside, not creating inside.
  • Why? Makes code flexible, testable, and less dependent.
  • When? Use when you want to swap implementations easily.
  • Example without DI ❌
    • Direct creation of MySQLDatabase
      class Application {
          private Database db = new MySQLDatabase(); // Yikes!
      }
    • Can’t change database easily
  • Example with DI βœ…
    • Inject Database through constructor of Application
      Application app1 = new Application(
                              new MySQLDatabase());
      Application app2 = new Application(
                              new MongoDatabase());
    • Decoupled: Application depends on interface, not specific class.

πŸ“Œ Bad Example ❌

class MySQLDatabase {
    public void connect() {
        System.out.println("Connecting to MySQL Database");
    }
}

class Application {

    //Database is a dependency of Application!
    private MySQLDatabase db = new MySQLDatabase();
    
    public void start() {
        db.connect();
        //Do whatever you want!
    }
    
}

public class DependencyInjectionExample {
        
    public static void main(String[] args) {
        Application app = new Application();
        app.start();
    }

}

πŸ“Œ Good Example βœ…

interface Database {
    void connect();
}

class MySQLDatabase implements Database {
    public void connect() {
        System.out.println("Connected to MySQL.");
    }
}

class MongoDatabase implements Database {
    public void connect() {
        System.out.println("Connected to MongoDB.");
    }
}

class Application {
    private Database db;

    public Application(Database db) {
        this.db = db;
    }

    public void start() {
        db.connect();
    }
}

public class Main {
    public static void main(String[] args) {
        Database mySQL = new MySQLDatabase();
        Application app1 = new Application(mySQL);
        app1.start();

        Database mongo = new MongoDatabase();
        Application app2 = new Application(mongo);
        app2.start();
    }
}

Explain a few different scenarios of using Interfaces in Java #


πŸ“Œ 1. Defining Common Behavior for Unrelated Classes

  • What? Different classes that should share the same behavior.
  • Example: Flyable interface for birds and airplanes.
    interface Flyable {
        void fly();
    }
    
    class Bird implements Flyable {
        public void fly() {
            System.out.println(
                "Bird flaps its wings to fly.");
        }
    }
    
    class Airplane implements Flyable {
        public void fly() {
            System.out.println(
                "Airplane flies using engines.");
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            Flyable bird = new Bird();
            bird.fly();
    
            Flyable plane = new Airplane();
            plane.fly();
        }
    }

πŸ“Œ 2. Achieving Multiple Inheritance

  • What? Java does not support multiple inheritance with classes, but it allows multiple interfaces.
  • Example: A SmartDevice that acts as both Camera and Phone.
    interface Camera {
        void takePhoto();
    }
    
    interface Phone {
        void makeCall(String number);
    }
    
    class SmartPhone implements Camera, Phone {
        public void takePhoto() {
            System.out.println("Photo taken.");
        }
    
        public void makeCall(String number) {
            System.out.println("Calling " + number);
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            SmartPhone phone = new SmartPhone();
            phone.takePhoto();
            phone.makeCall("123-456-7890");
        }
    }

πŸ“Œ 3. Implementing Dependency Injection

  • What? Allows flexibility by programming to an interface instead of a concrete class.
  • Example: Database interface for switching between MySQLDatabase and MongoDatabase.
    interface Database {
        void connect();
    }
    
    class MySQLDatabase implements Database {
        public void connect() {
            System.out.println("Connected to MySQL.");
        }
    }
    
    class MongoDatabase implements Database {
        public void connect() {
            System.out.println("Connected to MongoDB.");
        }
    }
    
    class Application {
        private Database db;
    
        public Application(Database db) {
            this.db = db;
        }
    
        public void start() {
            db.connect();
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            Database mySQL = new MySQLDatabase();
            Application app1 = new Application(mySQL);
            app1.start();
    
            Database mongo = new MongoDatabase();
            Application app2 = new Application(mongo);
            app2.start();
        }
    }

πŸ“Œ 4. Creating Callbacks (Observer Pattern)

  • What? Enables event-driven programming where one class notifies another.
  • Example: Listener interface for handling button clicks.
    interface OnClickListener {
        void onClick();
    }
    
    class Button {
        private OnClickListener listener;
    
        public void setOnClickListener(
                    OnClickListener listener) {
            this.listener = listener;
        }
    
        public void click() {
            if (listener != null) {
                listener.onClick();
            }
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            Button button = new Button();
    
            button.setOnClickListener(new OnClickListener() {
                public void onClick() {
                    System.out.println("Button clicked!");
                }
            });
    
            button.click();
        }
    }

πŸ“Œ 5. Defining Strategy Pattern for Flexible Algorithms

  • What? Allows switching between different behaviors at runtime.
  • Example: Different payment methods implementing PaymentStrategy.
    interface PaymentStrategy {
        void pay(int amount);
    }
    
    class CreditCardPayment implements PaymentStrategy {
        public void pay(int amount) {
            System.out.println("Paid $" 
                    + amount + " using Credit Card.");
        }
    }
    
    class PayPalPayment implements PaymentStrategy {
        public void pay(int amount) {
            System.out.println("Paid $" 
                    + amount + " using PayPal.");
        }
    }
    
    class ShoppingCart {
        private PaymentStrategy paymentStrategy;
    
        public ShoppingCart(PaymentStrategy paymentStrategy) {
            this.paymentStrategy = paymentStrategy;
        }
    
        public void checkout(int amount) {
            paymentStrategy.pay(amount);
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            ShoppingCart cart1 
                = new ShoppingCart(new CreditCardPayment());
            cart1.checkout(100);
    
            ShoppingCart cart2 
                = new ShoppingCart(new PayPalPayment());
            cart2.checkout(200);
        }
    }

Give different usecases for inheritance in Java #


Here are a few usecases for using inheritance in Java:

πŸ“Œ 1. Code Reusability

  • What? Allows a class to reuse properties and methods of another class.
  • Why? Avoids duplicate code and promotes efficient programming.
  • Example: Car and Bike inherit common behavior from Vehicle.
    class Vehicle {
        void start() {
            System.out.println("Vehicle is starting...");
        }
    }
    
    class Car extends Vehicle {
        void drive() {
            System.out.println("Car is driving...");
        }
    }
    
    class Bike extends Vehicle {
        void ride() {
            System.out.println("Bike is riding...");
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            Car car = new Car();
            car.start();
            car.drive();
    
            Bike bike = new Bike();
            bike.start();
            bike.ride();
        }
    }

πŸ“Œ 2. Method Overriding (Polymorphism)

  • What? Allows a subclass to provide a different implementation of a method.
  • Why? Enables dynamic method behavior based on object type.
  • Example: Dog and Cat override makeSound() from Animal.
    class Animal {
        void makeSound() {
            System.out.println("Animal makes a sound...");
        }
    }
    
    class Dog extends Animal {
        void makeSound() {
            System.out.println("Dog barks...");
        }
    }
    
    class Cat extends Animal {
        void makeSound() {
            System.out.println("Cat meows...");
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            Animal myDog = new Dog();
            myDog.makeSound();
    
            Animal myCat = new Cat();
            myCat.makeSound();
        }
    }

πŸ“Œ 3. Extending Functionality

  • What? Allows adding new behavior to an existing class.
  • Why? Enhances the base class without modifying it.
  • Example: ElectricCar extends Car to add battery functionality.
    class Car {
        void drive() {
            System.out.println("Car is driving...");
        }
    }
    
    class ElectricCar extends Car {
        void chargeBattery() {
            System.out.println("Battery is charging...");
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            ElectricCar tesla = new ElectricCar();
            tesla.drive();
            tesla.chargeBattery();
        }
    }

πŸ“Œ Summary Table

Need Example Purpose
1. Code Reusability Vehicle β†’ Car, Bike Avoids code duplication by reusing methods.
2. Method Overriding Animal β†’ Dog, Cat Allows different behavior in subclasses.
3. Extending Functionality Car β†’ ElectricCar Enhances a base class without modifying it.

What is Encapsulation? #


πŸ“Œ Overview

  • Bundling data (variables) and methods (functions) into a single unit
  • Restricts direct access to data
    • Allows controlled modifications through methods.

πŸ“Œ Bad Code: No Encapsulation

  • Problem: Direct access to balance allows invalid values.
    class BankAccount {
        public double balance;
    }
    
    public class Main {
        public static void main(String[] args) {
            BankAccount account = new BankAccount();
            account.balance = -1000;  // Invalid state
            System.out.println("Balance: " + account.balance);
        }
    }

πŸ“Œ Intermediate Code: Getters and Setters Without Validation

class BankAccount {
    private double balance;

    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        // No validation
        this.balance = balance;
    }
}

public class Main {
    public static void main(String[] args) {
        BankAccount account = new BankAccount();
        
        // Still allows invalid values
        account.setBalance(-1000);  
        
        System.out.println("Balance: " 
                + account.getBalance());
    }
}

Why Is This Still Not Ideal?

  • Uses private fields with getters and setters (better than public fields).
  • (HOWEVER) No validation in setBalance(), allowing negative values.
  • (HOWEVER) Allows setting any balance without controlled deposit/withdraw methods.

πŸ“Œ βœ… Improved Code: Encapsulation

  • Use private fields
  • Add getters and setters as needed
  • Add methods to perform other operations with necessary validations
    class BankAccount {
        private double balance;
    
        public void deposit(double amount) {
            if (amount > 0) {
                balance += amount;
            }
        }
    
        public void withdraw(double amount) {
            if (amount > 0 && amount <= balance) {
                balance -= amount;
            }
        }
    
        public double getBalance() {
            return balance;
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            BankAccount account = new BankAccount();
            account.deposit(500);
            account.withdraw(100);
            System.out.println("Balance: " 
                        + account.getBalance());
        }
    }

Advantages of Encapsulation #


Encapsulation improves maintainability, and flexibility in object-oriented programming.

1. Data Hiding (Prevents Direct Modification)

  • Bad Code: balance is public, allowing direct access
  • Improved Code: balance is private, preventing direct modification
    // ❌ Bad: Allows setting an invalid balance
    account.balance = -1000;  
    
    // βœ… Improved: Direct modification is restricted
    account.deposit(500);
    account.withdraw(100);

2. Controlled Access to Data

  • Bad Code: Getters and setters allow setting an invalid balance.
  • Improved Code: deposit() and withdraw() validate transactions before modifying balance.
  • Advantage: Prevents accidental or malicious modifications.
    // ❌ Bad: No validation in setter
    // Allows invalid values
    account.setBalance(-1000);  
    
    // βœ… Improved: Only valid transactions are allowed
    account.deposit(500);
    // Will not withdraw more than available balance
    account.withdraw(600);  

3. Increased Maintainability & Flexibility

  • Bad Code: Direct access to balance means changing the logic requires modifying multiple parts of the program.
  • Improved Code: Encapsulated logic allows easy modifications inside deposit() or withdraw().
  • Advantage: If business rules change, updates happen in one place instead of throughout the program.
    // βœ… Improved: We can easily add new rules
    public void deposit(double amount) {
        // Added new rule: Max deposit limit
        if (amount > 0 && amount < 10000) {
            balance += amount;
        }
    }

Encapsulation vs Security #


Concept Encapsulation Security
Purpose Prevent accidental modification of data in your code Protect data from unauthorized access
How It Works Uses private variables and controlled access via methods Uses encryption, authentication, and authorization
Example Prevents direct modification of score in a CricketScorer class Passwords stored in a database are encrypted so no one can read them

Explain Encapsulation in Java Using ArrayList as an example #


πŸ“Œ 1: How Data is Stored in ArrayList

  • ArrayList internally uses a dynamic array (elementData) to store elements.
  • This array is private, meaning it cannot be accessed or modified directly from outside the class.
  • The size of the list is managed separately to track the number of elements.
  • ArrayList prevents direct access to elementData, ensuring safety and consistency.
  • JDK Implementation (ArrayList.java):
    public class ArrayList<E> 
            extends AbstractList<E> 
            implements List<E>, RandomAccess, Cloneable {
        
        // Internal storage array (Encapsulated - private)
        private transient Object[] elementData;
    
        // Current number of elements in the list
        private int size;
    
    }

πŸ“Œ 2: Operations Defined Without Direct Access to Data

  • Since elementData is private, Java provides public methods to safely modify and access elements.
  • These methods validate inputs, manage memory, and prevent illegal operations.
  • Example Operation: Adding Elements (add())
    • The add() method increases the size of ArrayList and stores the element safely.
    • Ensures capacity before adding a new element.
    • Users can only add elements through add(), preventing direct modification of elementData
    public boolean add(E e) {
        
        // Check if array needs resizing
        ensureCapacityInternal(size + 1); 
        
        // Store element safely
        elementData[size++] = e; 
        
        return true;
    
    }
    
    // Ensures there is enough space for new elements
    private void ensureCapacityInternal(int minCapacity) {
    
        if (minCapacity - elementData.length > 0) {
            // Increases array size if needed
            grow(minCapacity); 
        }
    
    }

Accessing Elements (get())

  • The get() method retrieves an element from elementData while checking for valid indexes.
    public E get(int index) {
        rangeCheck(index); // Validate index
        return (E) elementData[index]; // Return element
    }
    
    // Ensures index is within valid range
    private void rangeCheck(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException();
    }

πŸ“Œ 3: Summary

Encapsulation Feature Implementation in ArrayList Purpose
Private Data private transient Object[] elementData; Prevents direct modification of internal array.
Controlled Access add(E e), get(int index), remove(int index) Allows safe modification and retrieval.
Validation Methods rangeCheck(index), ensureCapacityInternal(int minCapacity) Prevents illegal operations (e.g., out-of-bounds access).

What is Abstraction? #


πŸ“Œ Overview

  • Abstraction hides complexity
    • Only show what is necessary
  • The user interacts with the essential parts
    • While the implementation details remain hidden

πŸ“Œ Real-Life Examples of Abstraction

  • Driving a Car: You turn the steering wheel and press the accelerator
    • You don’t need to know how the engine, fuel injection, or transmission work.
  • Printing a Document: You click "Print" on your computer.
    • You don’t need to know how the printer processes the data or converts it into ink on paper.

πŸ“Œ Abstraction in Software Engineering

  • Layered Architecture in Applications: Modern applications follow a layered approach to abstract complexity.
    • The Web Layer doesn’t need to know how the Database Layer retrieves data.
      Layer Purpose
      Web Layer Displays the webpage to the user
      Business Logic Layer Has the Business Logic
      Data Layer Retrieves and stores data in the database

πŸ“Œ Abstraction in Programming

  • High Level Languages: Computers only understand binary (0s and 1s).
    • Instead of writing machine code, we write Java Code
    • The complexity of converting Java code and running it is abstracted away from us
  • Writing SQL Queries: SQL abstracts database operations.
    • You write a simple query like:SELECT * FROM users WHERE age > 18
    • The database handles how the query is executed behind the scenes
  • Calling Built-In Methods: Use library methods
    • When calling Math.sqrt(9), you don’t see how Java calculates the square root.
    • The internal implementation is abstracted away.

πŸ“Œ Summary

  • Abstraction simplifies usage by hiding low-level details.
  • Allows focus on essential functionality without worrying about implementation.
  • Used in everyday life, software architecture, and programming languages.

What is Cohesion? #


πŸ“Œ Overview

  • What?: Cohesion measures how closely related the responsibilities of a class are.
  • High Cohesion: A class has a single, well-defined responsibility.
  • Low Cohesion: A class has multiple unrelated responsibilities.

πŸ“Œ ❌ Example of Low Cohesion

public class DownloadAndStore {
    public void downloadFromInternet() {
        // Code to download data from the internet
    }

    public void parseData() {
        // Code to parse the downloaded data
    }

    public void storeInDatabase() {
        // Code to store the parsed data in the database
    }

    public void doEverything() {
        downloadFromInternet();
        parseData();
        storeInDatabase();
    }
}
  • Multiple Responsibilities: This class downloads data, parses it, and stores it.
  • Tightly Coupled: Changes in downloading logic may affect parsing and storage.
  • Difficult to Reuse: If another part of the program needs only specific feature, it might be difficult to reuse.

πŸ“Œ βœ… Example of High Cohesion

public class InternetDownloader {
    public Data downloadFromInternet() {
        // Code to download data from the internet
        return new Data();
    }
}

public class DataParser {
    public ParsedData parseData(Data data) {
        // Code to parse the downloaded data
        return new ParsedData();
    }
}

public class DatabaseStorer {
    public void storeInDatabase(ParsedData parsedData) {
        // Code to store the parsed data in the database
    }
}

public class DownloadAndStore {
    private InternetDownloader downloader 
                            = new InternetDownloader();
    private DataParser parser = new DataParser();
    private DatabaseStorer storer = new DatabaseStorer();

    public void doEverything() {
        Data data = downloader.downloadFromInternet();
        ParsedData parsedData = parser.parseData(data);
        storer.storeInDatabase(parsedData);
    }
}
  • Single Responsibility: Each class performs a specific task.
  • Easier to Test: Each class can be tested separately.
  • Improved Reusability: The InternetDownloader class can be used to download other data as well.

πŸ“Œ How ArrayList Maintains High Cohesion?

Cohesion Principle How ArrayList Implements It
Single Responsibility Only manages list operations (adding, retrieving, removing elements).
Encapsulated Data elementData is private; accessed via methods.
No Unrelated Methods No file handling, networking, or extra logic.

What is Coupling? #


πŸ“Œ Overview

  • What?: Coupling measures how much a class depends on another class.
  • Tight Coupling: A class is tightly linked to another. Changes in one impacts other.
  • Loose Coupling: A class has minimal dependencies. Changes are easy to make.

πŸ“Œ Simple Example of Tight Coupling

  • Problem: OrderProcessor is tightly coupled to PayPalPayment. If we need another payment method, we must modify OrderProcessor.
    class PayPalPayment {
        void processPayment(int amount) {
            System.out.println(
                "Processing PayPal payment of $" + amount);
        }
    }
    
    class OrderProcessor {
        private PayPalPayment payment = new PayPalPayment();
    
        void checkout(int amount) {
            payment.processPayment(amount);
        }
    }

πŸ“Œ Improved - Loose Coupling

  • Solution: Use an interface (PaymentMethod) to allow multiple implementations.
    interface PaymentMethod {
        void processPayment(int amount);
    }
    
    class PayPalPayment implements PaymentMethod {
        public void processPayment(int amount) {
            System.out.println(
                "Processing PayPal payment of $" 
                + amount);
        }
    }
    
    class CreditCardPayment implements PaymentMethod {
        public void processPayment(int amount) {
            System.out.println(
                "Processing Credit Card payment of $" 
                + amount);
        }
    }
    
    class OrderProcessor {
        private PaymentMethod payment;
    
        OrderProcessor(PaymentMethod payment) {
            this.payment = payment;
        }
    
        void checkout(int amount) {
            payment.processPayment(amount);
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            OrderProcessor order1 
                = new OrderProcessor(new PayPalPayment());
            order1.checkout(100);
    
            OrderProcessor order2 
                = new OrderProcessor(new CreditCardPayment());
            order2.checkout(200);
        }
    }

πŸ“Œ Complex Example of Tight Coupling

public class ShoppingCartEntry {
    public double price;
    public int quantity;
}

public class Order {
    private ShoppingCartEntry[] items;
    private double salesTax;

    public double calculateTotalPrice() {
        double totalPrice = 0;
        for (ShoppingCartEntry item : items) {
            totalPrice += item.price * item.quantity;
        }
        return totalPrice + salesTax;
    }
}
  • Direct Dependency: Order directly accesses ShoppingCartEntry fields.
  • Tightly Coupled: Changes in ShoppingCartEntry might affect Order.

πŸ“Œ Solution: Reducing Coupling

public class ShoppingCartEntry {
    private double price;
    private int quantity;

    public double getTotalPrice() {
        return price * quantity;
    }
}

public class ShoppingCart {
    private List<ShoppingCartEntry> items;

    public double getTotalPrice() {
        double totalPrice = 0;
        for (ShoppingCartEntry item : items) {
            totalPrice += item.getTotalPrice();
        }
        return totalPrice;
    }
}

public class Order {
    private ShoppingCart cart;
    private double salesTax;

    public double calculateTotalPrice() {
        return cart.getTotalPrice() + salesTax;
    }
}
  • Encapsulation: ShoppingCartEntry handles its own pricing logic.
  • Improved Maintainability: Order no longer depends on ShoppingCartEntry.

How to Reduce Coupling in Java? #


πŸ“Œ 1: Use Interfaces and Dependency Injection

  • Bad Example (Tight Coupling)
    • Car is tightly coupled to Engine.
    • If Engine changes or a new engine type is needed, Car must also change.
    class Engine {
        void start() {
            System.out.println("Engine starting...");
        }
    }
    
    class Car {
        // Direct dependency
        private Engine engine = new Engine(); 
    
        void drive() {
            engine.start();
            System.out.println("Car is driving...");
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            Car car = new Car();
            car.drive();
        }
    }
  • βœ… Good Example (Loose Coupling - Using Interfaces and DI)
    • Car depends on Engine interface, not a specific implementation.
    • Easily switch engines without modifying Car.
    interface Engine {
        void start();
    }
    
    class PetrolEngine implements Engine {
        public void start() {
            System.out.println("Petrol engine starting...");
        }
    }
    
    class DieselEngine implements Engine {
        public void start() {
            System.out.println("Diesel engine starting...");
        }
    }
    
    class Car {
        private Engine engine;
    
        // Constructor injection (Dependency Injection)
        public Car(Engine engine) {
            this.engine = engine;
        }
    
        void drive() {
            engine.start();
            System.out.println("Car is driving...");
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            Engine petrol = new PetrolEngine();
            Car car1 = new Car(petrol);
            car1.drive();
    
            Engine diesel = new DieselEngine();
            Car car2 = new Car(diesel);
            car2.drive();
        }
    }

πŸ“Œ 2: Use Factory Pattern Instead of Hardcoding Dependencies

  • Bad Example (Tight Coupling - Hardcoded Object Creation)
    • Application is tightly coupled to MySQLDatabase.
    • Cannot easily switch to PostgreSQL or other databases.
    class MySQLDatabase {
        void connect() {
            System.out.println("Connected to MySQL.");
        }
    }
    
    class Application {
        private MySQLDatabase database 
            = new MySQLDatabase(); // Hardcoded dependency
    
        void start() {
            database.connect();
            System.out.println("Application started.");
        }
    }
  • βœ… Good Example (Loose Coupling - Using Factory Pattern)
    • Application now depends on Database, not a specific implementation.
    • Can easily switch databases without modifying Application class.
    • Factory Pattern manages object creation.
    interface Database {
        void connect();
    }
    
    class MySQLDatabase implements Database {
        public void connect() {
            System.out.println("Connected to MySQL.");
        }
    }
    
    class PostgreSQLDatabase implements Database {
        public void connect() {
            System.out.println("Connected to PostgreSQL.");
        }
    }
    
    // Factory to create Database instances
    class DatabaseFactory {
        public static Database getDatabase(String type) {
            if (type.equalsIgnoreCase("MySQL")) {
                return new MySQLDatabase();
            } else if (type.equalsIgnoreCase("PostgreSQL")) {
                return new PostgreSQLDatabase();
            }
            throw new IllegalArgumentException(
                                "Unknown database type");
        }
    }
    
    class Application {
        private Database database;
    
        // Constructor Injection
        public Application(Database database) {
            this.database = database;
        }
    
        void start() {
            database.connect();
            System.out.println("Application started.");
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            Database db = DatabaseFactory.getDatabase("MySQL");
            Application app = new Application(db);
            app.start();
        }
    }

Abstraction vs Coupling vs Cohesion vs Encapsulation #


Remember: There is a great deal of intersection between these concepts!

Concept Definition Goal How to Achieve Example
Encapsulation Bundling data (variables) and methods (functions) into a single unit Prevent unintended changes Private variables, well designed instance methods, getters, setters elementData[] in ArrayList is private and modified through methods
Abstraction Hides implementation details and exposes only what’s necessary Reduce complexity Encapsulation, Interface, .. Programming in a high level language
Coupling Measures how dependent a class is on another Loose Coupling - Improve maintainability Use well-defined interfaces Car class can work with any implementation of Engine interface
Cohesion Measures how well a class focuses on a single responsibility High Cohesion - Increase maintainability Split unrelated responsibilities into separate classes Data Parser only parses data, Database Storer only stores data

Favor Composition Over Inheritance #


πŸ“Œ Overview

  • Inheritance and composition are two options to reuse code
  • However, composition is generally preferred unless there is a clear "IS-A" relationship.
  • Why?
    • Provides greater flexibility
    • Reduces tight coupling between classes

πŸ“Œ What is Composition?

  • Composition: "has-a" relationship where an object contains another object
  • Allows objects to delegate behavior to other objects rather than inheriting behavior from a parent class.

πŸ“Œ Bad Example For Inheritance

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

class Car extends Engine {
    public void drive() {
        System.out.println("Car is driving...");
    }
}

public class Main {
    public static void main(String[] args) {
        Car myCar = new Car();
        myCar.start(); // Inherited from Engine
        myCar.drive();
    }
}

Problems with the Above Approach

  • Incorrect Relationship: A Car is not a type of Engine, but it has an engine.
    • Using inheritance (extends Engine) creates an incorrect "IS-A" relationship instead of a "HAS-A" relationship.
  • Unnecessary Method Inheritance: Every method in Engine is automatically inherited by Car, even if it doesn't logically belong to a car.
    • Example: If Engine has a method changeOil(), Car will inherit it, even though a car itself does not change oilβ€”the engine does.
  • Limited Flexibility & Maintainability: What if we need to support multiple engine types like PetrolEngine or ElectricEngine?
    • With inheritance, we'd have to create multiple Car subclasses (PetrolCar, ElectricCar), making the system harder to extend and maintain.
    • A better approach is to use composition, where a Car contains an Engine object and delegates operations to it.

Better Approach: Composition

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

class Car {
    // Composition: Car has an Engine
    private Engine engine; 

    public Car(Engine engine) {
        this.engine = engine;
    }

    public void drive() {
        // Delegating behavior to Engine
        engine.start(); 
        System.out.println("Car is driving...");
    }
}

public class Main {
    public static void main(String[] args) {
        Engine myEngine = new Engine();
        Car myCar = new Car(myEngine);
        myCar.drive();
    }
}

πŸ“Œ When to Use Inheritance vs. Composition?

Criteria Use Inheritance Use Composition
Relationship Type When there is a true "is-a" relationship (e.g., Dog extends Animal) When there is a "has-a" relationship (e.g., Car has an Engine)
Code Reusability Inherits behavior from the parent class Reuses behavior by delegating it
Flexibility Creates a rigid relationship – subclasses inherit everything from the parent class More flexible – can choose which functionality to reuse

Final Recommendations

  • Use inheritance only when there is a true "is-a" relationship (e.g., Dog extends Animal).
  • Use composition for "has-a" relationships (e.g., Car has an Engine).
  • Avoid deep inheritance trees – They make code hard to maintain and understand.

What is Polymorphism? #


πŸ“Œ Overview

  • What?: Polymorphism allows the same method or function to behave differently
  • Meaning: Comes from the Greek words "poly" (many) and "morph" (forms).

πŸ“Œ Types of Polymorphism

  • Compile-Time Polymorphism (Method Overloading) β†’ Decided at compile time.
  • Run-Time Polymorphism (Method Overriding) β†’ Decided at runtime.

πŸ“Œ Example: Compile-Time Polymorphism (Method Overloading)

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

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

public class Main {
    public static void main(String[] args) {
        Calculator calc = new Calculator();
        System.out.println(calc.add(2, 3));
        System.out.println(calc.add(2.5, 3.5));
    }
}

πŸ“Œ Example: Run-Time Polymorphism (Method Overriding)

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

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

class Cat extends Animal {
    @Override
    public void sound() {
        System.out.println("Meow Meow");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal[] animals = {new Dog(), new Cat()};

        for (Animal animal : animals) {
            animal.sound();
        }
    }
}

πŸ“Œ Polymorphism works with interfaces as well

When a reference of an interface is used to hold objects of different classes implementing the interface, the method executed depends on the actual object:

Flyable[] flyingObjects = {new Bird(), new Aeroplane()};

for (Flyable object : flyingObjects) {
    object.fly();
}

This will print:

Bird flies by flapping wings
Aeroplane flies using engines.

Even though object.fly() is called on the same reference type, different implementations are executed.


What is Dynamic Method Dispatch? #


Overview

  • What?: Mechanism by which a call to an overridden method is resolved at runtime instead of compile time.
  • How?: Uses method overriding and a parent class reference that points to a child class object.

Example: Dynamic Method Dispatch in Action

class Parent {
    void show() {
        System.out.println("Parent class method");
    }
}

class Child extends Parent {
    @Override
    void show() {
        System.out.println("Child class method");
    }
}

public class Main {
    public static void main(String[] args) {
        
        // Parent reference, Child object
        Parent obj = new Child(); 

        obj.show(); // Output: Child class method
    }
}
  • The method execution is determined at runtime, based on the actual object (Child), not the reference type (Parent).
  • This enables loose coupling, where the parent reference can point to any subclass object.

Static vs Dynamic Binding #


Feature Static Binding Dynamic Binding
Definition Method call is resolved at compile time Method call is resolved at runtime
Type of Polymorphism Compile-time polymorphism (Method Overloading) Runtime polymorphism (Method Overriding)
Method Execution Based on reference type Based on actual object type
Performance Faster (resolved at compile time) Slower (resolved at runtime)
Example Static Method Calls Method overriding

Example: Static Binding (Compile-Time Resolution)

class MathOperations {
    static void multiply(int a, int b) {
        System.out.println("Multiplication: " + (a * b));
    }
}

public class Main {
    public static void main(String[] args) {
        // Output: Multiplication: 50
        MathOperations.multiply(5, 10); 
    }
}

Example: Dynamic Binding (Runtime Resolution)

class Parent {
    void display() {
        System.out.println("Parent display");
    }
}

class Child extends Parent {
    @Override
    void display() {
        System.out.println("Child display");
    }
}

public class Main {
    public static void main(String[] args) {
        Parent obj = new Child();
        obj.display(); // Output: Child display
    }
}

Key Takeaways

  • Polymorphism enables flexibility by allowing the same method call to perform different actions.
  • Dynamic Method Dispatch is the mechanism for achieving runtime polymorphism in Java.
  • Static Binding happens at compile time, while Dynamic Binding happens at runtime.

What is the need for Sealed Interfaces and Classes? #


πŸ“Œ Example: Sealed Class

  • Only allowed subclasses (Car, Truck, Bike) can extend Vehicle.
  • Car is final, so it cannot be extended further.
  • Truck is non-sealed, so any class can extend it.
  • Bike is sealed again, allowing only ElectricBike to extend it.
    sealed class Vehicle permits Car, Truck, Bike {} 
    
    // No further subclassing  
    final class Car extends Vehicle {}  
    
    // Can be extended freely  
    non-sealed class Truck extends Vehicle {}
    
    sealed class Bike extends Vehicle 
                      permits ElectricBike {}  
    
    // No further subclassing  
    final class ElectricBike extends Bike {}  

πŸ“Œ Example: Sealed Interface

  • Controls which classes can implement Animal.
  • Dog is final, so no other class can extend it.
  • Bird is non-sealed, so any class can implement it.
  • Cat is sealed, so only PersianCat can extend it.
    sealed interface Animal permits Dog, Cat, Bird {}  
    
    // Cannot be extended  
    final class Dog implements Animal {} 
    
    // Restricted to PersianCat  
    sealed class Cat implements Animal permits PersianCat {}  
    
    // Can be freely extended  
    non-sealed class Bird implements Animal {}  
    
    // No further subclassing  
    final class PersianCat extends Cat {}  

πŸ“Œ Summary:

  • Java New Feature: Introduced in Java 17
  • Restricts Inheritance: Only specified classes can extend or implement.
  • Prevents Unwanted Subclassing: No accidental extensions.
  • Improves Maintainability: Clearer type hierarchy.