Object-Oriented Programming (OOP) is a programming paradigm based on the concept of "objects" which contain data (attributes) and code (methods).

Key Concepts:
  • Objects: Instances of classes that represent real-world entities
  • Classes: Blueprints or templates for creating objects
  • Data (Attributes): Variables that hold the state of an object
  • Methods (Behavior): Functions that define what an object can do
Why OOP?
  • Modularity: Code is organized into independent, reusable pieces
  • Maintainability: Easier to update and modify code
  • Reusability: Classes can be reused across different programs
  • Scalability: Easy to extend functionality without breaking existing code
// Example: A simple class in Java class Car { // Attributes (Data) String brand; String color; int speed; // Method (Behavior) void accelerate() { speed += 10; } } // Creating an object Car myCar = new Car(); myCar.brand = "Toyota"; myCar.accelerate();

The four fundamental principles of OOP are:

1. Encapsulation

Bundling data and methods together and hiding internal implementation details.

2. Inheritance

Creating new classes from existing ones, inheriting attributes and methods.

3. Polymorphism

Objects of different types responding to the same method call in different ways.

4. Abstraction

Hiding complex implementation and showing only essential features.

Each of these pillars will be explored in detail in the following sections.

Class Object
Blueprint or template Instance of a class
Logical entity Physical entity
Declared once Created many times
No memory allocation Memory allocated when created
Example: Car (template) Example: myCar, yourCar (instances)
// Class Definition class Car { String brand; String model; } // Objects/Instances Car car1 = new Car(); // First object Car car2 = new Car(); // Second object

A constructor is a special method that is automatically called when an object is created. It initializes the object's state.

Key Features:
  • Same name as the class
  • No return type (not even void)
  • Called automatically when object is created
  • Can be overloaded (multiple constructors)
Types of Constructors:
1. Default Constructor

No parameters, provides default values

class Car { String brand; // Default constructor Car() { brand = "Unknown"; } }
2. Parameterized Constructor

Takes parameters to initialize object with specific values

class Car { String brand; String model; // Parameterized constructor Car(String b, String m) { brand = b; model = m; } } // Usage Car myCar = new Car("Toyota", "Camry");
3. Copy Constructor

Creates a new object as a copy of an existing object

class Car { String brand; // Copy constructor Car(Car other) { this.brand = other.brand; } }

Access modifiers control the visibility and accessibility of classes, methods, and variables.

Modifier Class Package Subclass World
public
protected
default
private
class BankAccount { // public - accessible everywhere public String accountNumber; // private - accessible only within class private double balance; // protected - accessible in subclasses protected String accountType; // Public method to access private data public double getBalance() { return balance; } }

Inheritance is a mechanism where a new class (child/subclass) derives properties and behavior from an existing class (parent/superclass).

Benefits:
  • Code Reusability: Avoid code duplication
  • Method Overriding: Child can modify parent's behavior
  • Hierarchical Classification: Represents real-world relationships
// Parent class class Animal { void eat() { System.out.println("Animal is eating"); } } // Child class inherits from Animal class Dog extends Animal { void bark() { System.out.println("Dog is barking"); } } // Usage Dog myDog = new Dog(); myDog.eat(); // Inherited from Animal myDog.bark(); // Own method
1. Single Inheritance

One child class inherits from one parent class.

class Vehicle { } class Car extends Vehicle { }
2. Multilevel Inheritance

Chain of inheritance (A → B → C).

class Animal { } class Mammal extends Animal { } class Dog extends Mammal { }
3. Hierarchical Inheritance

Multiple child classes inherit from one parent.

class Animal { } class Dog extends Animal { } class Cat extends Animal { }
4. Multiple Inheritance (Not in Java)

One child inherits from multiple parents. Not supported in Java to avoid ambiguity (Diamond Problem).

Java Alternative: Use interfaces

interface Flyable { void fly(); } interface Swimmable { void swim(); } class Duck implements Flyable, Swimmable { public void fly() { } public void swim() { } }
5. Hybrid Inheritance

Combination of two or more types of inheritance.

The super keyword refers to the immediate parent class object.

Uses:
1. Access Parent's Variables
class Parent { int num = 10; } class Child extends Parent { int num = 20; void display() { System.out.println(super.num); // 10 (parent's) System.out.println(num); // 20 (child's) } }
2. Call Parent's Methods
class Parent { void display() { System.out.println("Parent display"); } } class Child extends Parent { void display() { super.display(); // Call parent's display System.out.println("Child display"); } }
3. Call Parent's Constructor
class Parent { Parent(String msg) { System.out.println(msg); } } class Child extends Parent { Child() { super("Calling parent constructor"); } }

Method Overriding occurs when a child class provides a specific implementation for a method already defined in its parent class.

Rules:
  • Method signature must be the same
  • Return type must be same or covariant
  • Access modifier cannot be more restrictive
  • Cannot override final or static methods
class Animal { void makeSound() { System.out.println("Animal makes a sound"); } } class Dog extends Animal { @Override // Annotation (optional but recommended) void makeSound() { System.out.println("Dog barks"); } } // Usage Animal myAnimal = new Dog(); myAnimal.makeSound(); // Output: Dog barks

Polymorphism means "many forms". It allows objects of different types to be accessed through the same interface.

The word comes from Greek: poly (many) + morph (form)

Types:
  • Compile-time (Static): Method Overloading, Operator Overloading
  • Runtime (Dynamic): Method Overriding
// One method, many behaviors Animal animal1 = new Dog(); Animal animal2 = new Cat(); animal1.makeSound(); // Dog barks animal2.makeSound(); // Cat meows

Method Overloading allows multiple methods with the same name but different parameters in the same class.

Rules:
  • Same method name
  • Different number of parameters, OR
  • Different type of parameters, OR
  • Different order of parameters
Cannot overload by:
  • Return type alone
  • Access modifiers
class Calculator { // Different number of parameters int add(int a, int b) { return a + b; } int add(int a, int b, int c) { return a + b + c; } // Different type of parameters double add(double a, double b) { return a + b; } } // Usage Calculator calc = new Calculator(); calc.add(5, 10); // Calls int add(int, int) calc.add(5, 10, 15); // Calls int add(int, int, int) calc.add(5.5, 10.5); // Calls double add(double, double)

Method Overriding occurs when a subclass provides a specific implementation of a method already defined in its parent class.

Key Concepts:
  • Decided at runtime (dynamic binding)
  • Requires inheritance
  • Method signature must be identical
class Shape { void draw() { System.out.println("Drawing a shape"); } } class Circle extends Shape { @Override void draw() { System.out.println("Drawing a circle"); } } class Rectangle extends Shape { @Override void draw() { System.out.println("Drawing a rectangle"); } } // Usage - Runtime Polymorphism Shape shape1 = new Circle(); Shape shape2 = new Rectangle(); shape1.draw(); // Output: Drawing a circle shape2.draw(); // Output: Drawing a rectangle
Aspect Overloading Overriding
When Compile-time Runtime
Where Same class Inheritance required
Parameters Must be different Must be same
Return type Can be different Must be same (or covariant)
Binding Static binding Dynamic binding
Purpose Increase readability Provide specific implementation

Covariant return type allows an overriding method to return a subtype of the return type declared in the parent class.

class Animal { Animal reproduce() { return new Animal(); } } class Dog extends Animal { @Override Dog reproduce() { // Dog is subtype of Animal return new Dog(); } } // This is valid since Java 5.0
Benefits:
  • More specific return types
  • Avoids type casting
  • More flexible code

Abstraction is hiding the implementation details and showing only the essential features to the user.

Real-world Example:

When you drive a car, you only use steering wheel, pedals, and gears. You don't need to know how the engine works internally.

In Java, abstraction is achieved through:
  • Abstract Classes (0-100% abstraction)
  • Interfaces (100% abstraction)
Benefits:
  • Reduces complexity
  • Hides implementation details
  • Enhances security
  • Supports code maintenance

An abstract class is a class that cannot be instantiated and may contain abstract methods (methods without implementation).

Key Features:
  • Declared with abstract keyword
  • Cannot create objects directly
  • Can have abstract and non-abstract methods
  • Can have constructors
  • Can have instance variables
abstract class Animal { // Abstract method (no implementation) abstract void makeSound(); // Concrete method (with implementation) void sleep() { System.out.println("Animal is sleeping"); } } class Dog extends Animal { // Must implement abstract method @Override void makeSound() { System.out.println("Dog barks"); } } // Usage // Animal a = new Animal(); // ERROR: Cannot instantiate Animal a = new Dog(); // OK: Can reference subclass a.makeSound(); // Output: Dog barks a.sleep(); // Output: Animal is sleeping

An interface is a completely abstract class that contains only abstract methods (until Java 8).

Key Features:
  • 100% abstraction (by default)
  • All methods are public and abstract (before Java 8)
  • All fields are public, static, and final
  • Cannot be instantiated
  • A class can implement multiple interfaces
interface Animal { // Abstract method (public abstract by default) void makeSound(); void eat(); } class Dog implements Animal { @Override public void makeSound() { System.out.println("Dog barks"); } @Override public void eat() { System.out.println("Dog eats"); } } // Usage Animal myDog = new Dog(); myDog.makeSound(); myDog.eat();
Java 8+ Interface Features:
  • Default methods: Methods with implementation
  • Static methods: Utility methods
interface Vehicle { void start(); // Abstract method // Default method (Java 8+) default void honk() { System.out.println("Vehicle honks"); } // Static method (Java 8+) static void service() { System.out.println("Vehicle serviced"); } }
Feature Abstract Class Interface
Methods Abstract + Concrete All abstract (before Java 8)
Variables Can have any type Only public static final
Constructor Can have Cannot have
Multiple Can extend one class Can implement multiple
Access Modifiers Any modifier Public only (default)
When to use Common base with shared code Contract/behavior definition
When to use Abstract Class:
  • Share code among closely related classes
  • Need non-static, non-final fields
  • Need access modifiers other than public
When to use Interface:
  • Unrelated classes need to implement same methods
  • Specify behavior without implementation
  • Take advantage of multiple inheritance

Java doesn't support multiple inheritance with classes to avoid the Diamond Problem. However, a class can implement multiple interfaces.

interface Flyable { void fly(); } interface Swimmable { void swim(); } // A class implementing multiple interfaces class Duck implements Flyable, Swimmable { @Override public void fly() { System.out.println("Duck is flying"); } @Override public void swim() { System.out.println("Duck is swimming"); } } // Usage Duck duck = new Duck(); duck.fly(); duck.swim();
Diamond Problem:

When a class inherits from two classes that have the same method, which method should it inherit?

// This would cause ambiguity (Not allowed in Java) class A { void show() { } } class B extends A { void show() { } } class C extends A { void show() { } } // ERROR: Which show() should D inherit? class D extends B, C { }

Encapsulation is the bundling of data (variables) and methods that operate on that data into a single unit (class), and restricting direct access to some components.

Key Concepts:
  • Data Hiding: Make variables private
  • Access Control: Provide public getter/setter methods
  • Controlled Access: Validate data before setting
Benefits:
  • Data protection and security
  • Flexibility to change implementation
  • Control over data (validation)
  • Read-only or write-only properties
class BankAccount { // Private variables (data hiding) private String accountNumber; private double balance; // Public getter public double getBalance() { return balance; } // Public setter with validation public void deposit(double amount) { if (amount > 0) { balance += amount; } else { System.out.println("Invalid amount"); } } } // Usage BankAccount account = new BankAccount(); // account.balance = 1000000; // ERROR: Cannot access account.deposit(1000); // OK: Controlled access

Getters and Setters are methods that provide controlled access to private variables.

Naming Convention:
  • Getter: getVariableName()
  • Setter: setVariableName()
  • Boolean: isVariableName()
class Student { private String name; private int age; private boolean isEnrolled; // Getter for name public String getName() { return name; } // Setter for name public void setName(String name) { this.name = name; } // Getter for age with validation public int getAge() { return age; } // Setter for age with validation public void setAge(int age) { if (age > 0 && age < 120) { this.age = age; } else { System.out.println("Invalid age"); } } // Boolean getter (uses 'is' prefix) public boolean isEnrolled() { return isEnrolled; } // Boolean setter public void setEnrolled(boolean enrolled) { this.isEnrolled = enrolled; } }
Benefits:
  • Data validation before setting
  • Read-only properties (only getter)
  • Write-only properties (only setter)
  • Computed properties
Aspect Encapsulation Abstraction
Purpose Data hiding and protection Hide implementation details
Focus HOW to achieve WHAT to achieve
Level Data level Design level
Implementation Private variables + public methods Abstract classes + Interfaces
Example Private balance in BankAccount Animal interface with makeSound()
Simple Analogy:
  • Encapsulation: A capsule hides medicine inside
  • Abstraction: You know the capsule cures headache, but don't know how

The this keyword refers to the current object instance.

Uses:
1. Distinguish between instance variables and parameters
class Student { private String name; public void setName(String name) { this.name = name; // this.name = instance variable // name = parameter } }
2. Call another constructor
class Student { private String name; private int age; Student() { this("Unknown", 0); // Calls parameterized constructor } Student(String name, int age) { this.name = name; this.age = age; } }
3. Pass current object as parameter
class Student { void display() { print(this); // Pass current object } void print(Student s) { System.out.println(s.name); } }
4. Return current object
class Student { Student setName(String name) { this.name = name; return this; // Return current object (Method Chaining) } } // Method chaining student.setName("John").setAge(20).setGrade("A");

Let's understand all OOP concepts using a Car Manufacturing System.

1. Class and Object
  • Class: Car blueprint/design created by engineers
  • Object: Actual cars manufactured from that blueprint

Think of it like a cookie cutter (class) and cookies (objects).

// Class = Blueprint class Car { String brand; String model; int year; } // Objects = Actual cars manufactured Car car1 = new Car(); // First car car1.brand = "Toyota"; Car car2 = new Car(); // Second car car2.brand = "Honda";
2. Encapsulation

A car's engine is enclosed in a hood. You can't directly access internal parts.

  • You use the accelerator pedal (public method) to increase speed
  • The engine internals are hidden (private variables)
  • The pedal provides controlled access to the engine
class Car { // Private - Hidden from outside private int engineRPM; private double fuelLevel; // Public - Controlled access public void pressAccelerator() { if (fuelLevel > 0) { engineRPM += 500; // Internal logic hidden } } public double getFuelLevel() { return fuelLevel; } }
3. Inheritance

Different car types inherit from a base Vehicle design:

  • Vehicle (Parent): Common features - engine, wheels, steering
  • Car (Child): Inherits all + adds trunk, 4 doors
  • Truck (Child): Inherits all + adds cargo bed, towing capacity
// Parent class class Vehicle { protected String engineType; protected int wheels; void start() { System.out.println("Vehicle starting..."); } } // Child inherits everything from Vehicle class Car extends Vehicle { int doors; Car() { this.wheels = 4; // Inherited property this.doors = 4; } } class Truck extends Vehicle { double cargoCapacity; Truck() { this.wheels = 6; // Inherited property this.cargoCapacity = 1000; } }
4. Polymorphism

All vehicles have a start() method, but each starts differently:

  • Electric Car: Silent start, no ignition sound
  • Diesel Truck: Loud rumbling start
  • Sports Car: Roaring engine start

Same method name, different behaviors!

class Vehicle { void start() { System.out.println("Vehicle starting"); } } class ElectricCar extends Vehicle { @Override void start() { System.out.println("Silent start... *whirr*"); } } class DieselTruck extends Vehicle { @Override void start() { System.out.println("*ROARRR* Loud diesel engine!"); } } class SportsCar extends Vehicle { @Override void start() { System.out.println("*VROOOOM* Engine roaring!"); } } // Usage - Same method, different behavior Vehicle v1 = new ElectricCar(); Vehicle v2 = new DieselTruck(); Vehicle v3 = new SportsCar(); v1.start(); // Silent start v2.start(); // Loud diesel v3.start(); // Engine roaring
5. Abstraction

When you drive a car:

  • You use steering wheel, pedals, gear (interface)
  • You don't need to know how fuel injection, combustion, transmission work (hidden implementation)

Abstraction shows "WHAT" a car can do, not "HOW" it does it.

Encapsulation in Banking

Your bank account balance is private. You can't directly change it.

  • Private: Account balance
  • Public methods: deposit(), withdraw(), getBalance()
  • Bank validates every transaction
Inheritance in Banking
  • Account (Parent): accountNumber, balance, deposit(), withdraw()
  • SavingsAccount (Child): + interestRate, calculateInterest()
  • CurrentAccount (Child): + overdraftLimit, allowOverdraft()
Polymorphism in Banking

All accounts have calculateInterest(), but:

  • Savings: 4% interest
  • Fixed Deposit: 7% interest
  • Current: 0% interest
Abstraction in Banking

ATM interface shows:

  • Withdraw, Deposit, Check Balance (visible)
  • Complex backend processing hidden
Classes and Objects
  • Class: Menu (template for dishes)
  • Objects: Pizza, Burger, Pasta (specific dishes)
Encapsulation
  • Kitchen recipe is secret (private)
  • Customer orders through waiter (public interface)
  • No direct access to kitchen
Inheritance
  • MenuItem (Parent): name, price, description
  • Pizza (Child): + size, toppings
  • Beverage (Child): + volume, temperature
Polymorphism

All items have prepare() method:

  • Pizza: Bake in oven for 15 minutes
  • Salad: Mix ingredients, serve cold
  • Coffee: Brew and serve hot
Abstraction
  • Customer sees menu and prices
  • Cooking process is hidden
  • Just order and receive food!

A comprehensive example demonstrating all OOP concepts in an e-commerce system.

System Overview:
  • Encapsulation: User class protects password with validation
  • Abstraction: Product is abstract - defines contract for all products
  • Inheritance: Electronics and Clothing extend Product
  • Polymorphism: Each product calculates shipping differently

Real-world mapping: This mimics how Amazon/Flipkart categorize products and calculate shipping costs based on product type.

// Encapsulation Example class User { private String userId; private String password; private String email; public User(String userId, String email) { this.userId = userId; this.email = email; } // Controlled access with validation public void setPassword(String password) { if (password.length() >= 8) { this.password = password; } else { throw new IllegalArgumentException("Password too short"); } } public String getEmail() { return email; } } // Abstraction Example abstract class Product { protected String productId; protected String name; protected double price; public Product(String productId, String name, double price) { this.productId = productId; this.name = name; this.price = price; } // Abstract method - must be implemented by subclasses abstract double calculateShipping(); // Concrete method - common to all products public void displayInfo() { System.out.println("Product: " + name + ", Price: $" + price); } } // Inheritance Example class Electronics extends Product { private int warrantyMonths; public Electronics(String id, String name, double price, int warranty) { super(id, name, price); // Call parent constructor this.warrantyMonths = warranty; } // Implement abstract method @Override double calculateShipping() { return price * 0.05; // 5% of product price } } class Clothing extends Product { private String size; public Clothing(String id, String name, double price, String size) { super(id, name, price); this.size = size; } @Override double calculateShipping() { return 10.0; // Flat rate for clothing } } // Polymorphism Example class ShoppingCart { private List<Product> products = new ArrayList<>(); public void addProduct(Product product) { products.add(product); } public double calculateTotalShipping() { double total = 0; for (Product product : products) { // Runtime polymorphism - correct method called based on actual object type total += product.calculateShipping(); } return total; } } // Usage ShoppingCart cart = new ShoppingCart(); cart.addProduct(new Electronics("E001", "Laptop", 1000, 24)); cart.addProduct(new Clothing("C001", "T-Shirt", 20, "M")); System.out.println("Total Shipping: $" + cart.calculateTotalShipping());

Library system demonstrating interface implementation and abstract classes.

Design Patterns Used:
  • Interface: Borrowable defines common behavior for all library items
  • Abstract Class: LibraryItem implements common logic, leaves fees to subclasses
  • Template Method: checkout/return logic is same, late fees vary by item type

Why this design? Different items (books, DVDs, magazines) share borrowing behavior but have different late fee policies. Abstract class provides shared implementation while allowing customization.

// Interface for borrowable items interface Borrowable { void checkOut(String userId); void returnItem(); boolean isAvailable(); } // Abstract class for library items abstract class LibraryItem implements Borrowable { protected String itemId; protected String title; protected boolean available; protected String borrowedBy; public LibraryItem(String itemId, String title) { this.itemId = itemId; this.title = title; this.available = true; } @Override public void checkOut(String userId) { if (available) { this.borrowedBy = userId; this.available = false; System.out.println(title + " checked out by " + userId); } } @Override public void returnItem() { this.borrowedBy = null; this.available = true; System.out.println(title + " returned"); } @Override public boolean isAvailable() { return available; } // Abstract method for calculating late fees abstract double calculateLateFee(int daysLate); } // Concrete classes class Book extends LibraryItem { private String author; private int pages; public Book(String itemId, String title, String author, int pages) { super(itemId, title); this.author = author; this.pages = pages; } @Override double calculateLateFee(int daysLate) { return daysLate * 0.50; // $0.50 per day } } class DVD extends LibraryItem { private int duration; public DVD(String itemId, String title, int duration) { super(itemId, title); this.duration = duration; } @Override double calculateLateFee(int daysLate) { return daysLate * 2.00; // $2.00 per day } } // Usage demonstrating polymorphism List<LibraryItem> items = new ArrayList<>(); items.add(new Book("B001", "Java Programming", "John Doe", 500)); items.add(new DVD("D001", "OOP Tutorial", 120)); for (LibraryItem item : items) { item.checkOut("USER123"); System.out.println("Late fee (7 days): $" + item.calculateLateFee(7)); }

Payment processing system using the Strategy Pattern - a behavioral design pattern.

Why Use Interfaces Here?
  • Flexibility: Easy to add new payment methods (Bitcoin, UPI, etc.)
  • Loose Coupling: PaymentProcessor doesn't need to know payment details
  • Open/Closed Principle: Open for extension, closed for modification

Real-world usage: E-commerce platforms like Stripe, PayPal APIs use similar patterns to support multiple payment gateways without changing core business logic.

// Interface for payment methods interface PaymentMethod { boolean processPayment(double amount); String getPaymentDetails(); } // Multiple implementations of payment interface class CreditCard implements PaymentMethod { private String cardNumber; private String cvv; private String expiryDate; public CreditCard(String cardNumber, String cvv, String expiryDate) { this.cardNumber = cardNumber; this.cvv = cvv; this.expiryDate = expiryDate; } @Override public boolean processPayment(double amount) { System.out.println("Processing credit card payment: $" + amount); // Validate card, process payment return true; } @Override public String getPaymentDetails() { return "Credit Card ending in " + cardNumber.substring(cardNumber.length() - 4); } } class PayPal implements PaymentMethod { private String email; public PayPal(String email) { this.email = email; } @Override public boolean processPayment(double amount) { System.out.println("Processing PayPal payment: $" + amount); return true; } @Override public String getPaymentDetails() { return "PayPal account: " + email; } } class BitcoinWallet implements PaymentMethod { private String walletAddress; public BitcoinWallet(String walletAddress) { this.walletAddress = walletAddress; } @Override public boolean processPayment(double amount) { System.out.println("Processing Bitcoin payment: $" + amount); return true; } @Override public String getPaymentDetails() { return "Bitcoin wallet: " + walletAddress.substring(0, 8) + "..."; } } // Payment processor using polymorphism class PaymentProcessor { public void processOrder(double amount, PaymentMethod paymentMethod) { System.out.println("Payment method: " + paymentMethod.getPaymentDetails()); if (paymentMethod.processPayment(amount)) { System.out.println("Payment successful!"); } else { System.out.println("Payment failed!"); } } } // Usage - same method works with different payment types PaymentProcessor processor = new PaymentProcessor(); processor.processOrder(100.00, new CreditCard("1234-5678-9012-3456", "123", "12/25")); processor.processOrder(50.00, new PayPal("[email protected]")); processor.processOrder(200.00, new BitcoinWallet("1A2B3C4D5E6F7G8H"));

Notification system using the Strategy Pattern - allows runtime selection of notification method.

Design Benefits:
  • Runtime Flexibility: Change notification method on-the-fly
  • Single Responsibility: Each strategy handles one notification type
  • Easy Testing: Can mock strategies for unit tests
  • User Preferences: Users can choose preferred notification method

Real-world example: Apps like WhatsApp, Slack allow users to choose notification preferences (push, email, SMS) - implemented using strategy pattern.

// Strategy interface interface NotificationStrategy { void send(String recipient, String message); } // Concrete strategies class EmailNotification implements NotificationStrategy { @Override public void send(String recipient, String message) { System.out.println("Sending EMAIL to " + recipient); System.out.println("Subject: Notification"); System.out.println("Body: " + message); } } class SMSNotification implements NotificationStrategy { @Override public void send(String recipient, String message) { System.out.println("Sending SMS to " + recipient); System.out.println("Message: " + message.substring(0, Math.min(160, message.length()))); } } class PushNotification implements NotificationStrategy { @Override public void send(String recipient, String message) { System.out.println("Sending PUSH notification to device: " + recipient); System.out.println("Alert: " + message); } } // Context class class NotificationService { private NotificationStrategy strategy; public void setStrategy(NotificationStrategy strategy) { this.strategy = strategy; } public void sendNotification(String recipient, String message) { if (strategy == null) { throw new IllegalStateException("Notification strategy not set"); } strategy.send(recipient, message); } } // Usage - Change notification method at runtime NotificationService service = new NotificationService(); service.setStrategy(new EmailNotification()); service.sendNotification("[email protected]", "Your order has been shipped!"); service.setStrategy(new SMSNotification()); service.sendNotification("+1234567890", "Your order has been shipped!"); service.setStrategy(new PushNotification()); service.sendNotification("device123", "Your order has been shipped!");

Java is not considered 100% object-oriented because of primitive data types.

Reasons:
  • Primitive types exist: int, char, boolean, double, etc. are not objects
  • Not everything is an object: Primitives don't inherit from Object class
  • Static methods: Can be called without creating objects
// Not objects int num = 10; boolean flag = true; // Wrapper classes make them objects Integer numObj = Integer.valueOf(10); Boolean flagObj = Boolean.valueOf(true);
Languages that are 100% OO:

Smalltalk, Ruby - everything is an object, even primitives

No, we cannot override static methods. This is called method hiding, not overriding.

Why?
  • Static methods belong to the class, not objects
  • Resolved at compile-time (static binding)
  • Overriding requires runtime polymorphism
class Parent { static void display() { System.out.println("Parent static method"); } } class Child extends Parent { static void display() { // Method hiding, NOT overriding System.out.println("Child static method"); } } // Test Parent obj1 = new Parent(); Parent obj2 = new Child(); // Parent reference, Child object obj1.display(); // Output: Parent static method obj2.display(); // Output: Parent static method (NOT Child!) // Determined by reference type, not object type

No, private methods cannot be overridden.

Reason:
  • Private methods are not visible to subclasses
  • Overriding requires method to be accessible
  • You can have a method with the same name in child, but it's a new method
class Parent { private void display() { System.out.println("Parent private method"); } public void callDisplay() { display(); // Calls Parent's display } } class Child extends Parent { private void display() { // This is NOT overriding! System.out.println("Child private method"); } } Child child = new Child(); child.callDisplay(); // Output: Parent private method

No, constructors cannot be overridden.

Reasons:
  • Constructors are not inherited
  • Each class must have its own constructor
  • Constructor name must match class name

However, constructors can be overloaded.

class Car { String brand; // Constructor overloading (NOT overriding) Car() { brand = "Unknown"; } Car(String brand) { this.brand = brand; } }

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

// Diamond structure A / \ B C \ / D // If A has method foo() // Both B and C override foo() // Which foo() should D inherit?
Java's Solution:
  • Java doesn't support multiple inheritance with classes
  • Use interfaces instead
  • Java 8+ allows default methods in interfaces, bringing back the problem
  • Solution: Class must explicitly override the method
interface A { default void show() { System.out.println("A"); } } interface B { default void show() { System.out.println("B"); } } class C implements A, B { // Must override to resolve ambiguity @Override public void show() { A.super.show(); // Call A's version // or B.super.show(); // or provide own implementation } }
Inheritance: "IS-A" relationship

Dog IS-A Animal

class Animal { } class Dog extends Animal { } // Dog IS-A Animal
Composition: "HAS-A" relationship

Car HAS-A Engine

class Engine { void start() { System.out.println("Engine started"); } } class Car { private Engine engine; // Car HAS-A Engine public Car() { engine = new Engine(); } public void start() { engine.start(); } }
Aspect Inheritance Composition
Relationship IS-A HAS-A
Coupling Tight coupling Loose coupling
Flexibility Less flexible More flexible
When to use When subclass truly is a type of parent When you need functionality without IS-A

Best Practice: "Favor composition over inheritance" - Design Patterns book

SOLID principles are five design principles for writing maintainable and scalable OOP code.

S - Single Responsibility Principle

A class should have only one reason to change.

// BAD: Multiple responsibilities class Employee { void calculateSalary() { } void saveToDatabase() { } void generateReport() { } } // GOOD: Single responsibility class Employee { void calculateSalary() { } } class EmployeeRepository { void saveToDatabase(Employee emp) { } } class ReportGenerator { void generateReport(Employee emp) { } }
O - Open/Closed Principle

Open for extension, closed for modification.

// Use inheritance/interfaces to extend, not modify abstract class Shape { abstract double area(); } class Circle extends Shape { double area() { /* calculate */ } } class Rectangle extends Shape { double area() { /* calculate */ } } // Can add new shapes without modifying existing code
L - Liskov Substitution Principle

Subclasses should be substitutable for their base classes.

// Should work correctly Animal animal = new Dog(); animal.makeSound(); // Should work as expected
I - Interface Segregation Principle

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

// BAD: Fat interface interface Worker { void work(); void eat(); void sleep(); } // GOOD: Segregated interfaces interface Workable { void work(); } interface Eatable { void eat(); }
D - Dependency Inversion Principle

Depend on abstractions, not concretions.

// BAD: Depends on concrete class class LightBulb { void turnOn() { } } class Switch { private LightBulb bulb; } // GOOD: Depends on abstraction interface Switchable { void turnOn(); } class Switch { private Switchable device; // Can be any switchable device }
1. Association

A general relationship between two classes. Objects can exist independently.

class Teacher { } class Student { } // Teacher and Student are associated // Both can exist independently
2. Aggregation (Weak "HAS-A")

Special form of association. Child can exist independently of parent.

class Department { private List<Teacher> teachers; } // Department HAS teachers // But teachers can exist without department
3. Composition (Strong "HAS-A")

Strong ownership. Child cannot exist without parent.

class House { private Room room; // Room is part of House public House() { room = new Room(); // Created with House } } // If House is destroyed, Room is also destroyed
Type Relationship Lifetime Example
Association Uses Independent Teacher-Student
Aggregation Has-A (weak) Independent Department-Teacher
Composition Part-Of (strong) Dependent House-Room
Tight Coupling

Classes are highly dependent on each other. Changes in one affect the other.

// BAD: Tightly coupled class Database { void save() { // MySQL specific code } } class UserService { private Database db = new Database(); // Direct dependency void saveUser() { db.save(); } } // Problem: Cannot easily switch to PostgreSQL
Loose Coupling

Classes are independent. Changes in one don't affect the other.

// GOOD: Loosely coupled interface Database { void save(); } class MySQL implements Database { public void save() { } } class PostgreSQL implements Database { public void save() { } } class UserService { private Database db; // Depends on interface public UserService(Database db) { this.db = db; // Injected from outside } void saveUser() { db.save(); } } // Can easily switch database implementations UserService service = new UserService(new MySQL()); // Or UserService service = new UserService(new PostgreSQL());
Benefits of Loose Coupling:
  • Easier to maintain
  • Easier to test (can use mocks)
  • More flexible
  • Supports dependency injection
1. final (keyword)

Used to declare constants, prevent inheritance, and prevent method overriding.

// final variable - cannot be changed final int MAX_SIZE = 100; // final method - cannot be overridden class Parent { final void display() { } } // final class - cannot be inherited final class Math { // No class can extend Math }
2. finally (block)

Used in exception handling. Always executes after try-catch.

try { // Code that may throw exception int result = 10 / 0; } catch (Exception e) { // Handle exception } finally { // Always executes (cleanup code) System.out.println("This always runs"); }
3. finalize() (method)

Called by garbage collector before object is destroyed. Deprecated in Java 9+

class MyClass { @Override protected void finalize() throws Throwable { // Cleanup before garbage collection System.out.println("Object is being destroyed"); } }
Term Type Purpose
final Keyword Make constants, prevent inheritance/overriding
finally Block Execute code after try-catch
finalize Method Cleanup before garbage collection (deprecated)