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
, ...).
- Interfaces β Define common behavior (
π 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
- Direct creation of
- Example with DI β
- Inject
Database
through constructor ofApplication
Application app1 = new Application( new MySQLDatabase()); Application app2 = new Application( new MongoDatabase());
- Decoupled:
Application
depends on interface, not specific class.
- Inject
π 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 bothCamera
andPhone
.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 betweenMySQLDatabase
andMongoDatabase
.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
andBike
inherit common behavior fromVehicle
.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
andCat
overridemakeSound()
fromAnimal
.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
extendsCar
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
ispublic
, allowing direct access - Improved Code:
balance
isprivate
, 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()
andwithdraw()
validate transactions before modifyingbalance
. - 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()
orwithdraw()
. - 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 ofArrayList
and stores the element safely. - Ensures capacity before adding a new element.
- Users can only add elements through
add()
, preventing direct modification ofelementData
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); } }
- The
Accessing Elements (get()
)
- The
get()
method retrieves an element fromelementData
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
- The Web Layer doesnβt need to know how the Database Layer retrieves data.
π 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
- You write a simple query like:
- 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.
- When calling
π 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 toPayPalPayment
. If we need another payment method, we must modifyOrderProcessor
.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 accessesShoppingCartEntry
fields. - Tightly Coupled: Changes in
ShoppingCartEntry
might affectOrder
.
π 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 onShoppingCartEntry
.
How to Reduce Coupling in Java? #
π 1: Use Interfaces and Dependency Injection
- Bad Example (Tight Coupling)
Car
is tightly coupled toEngine
.- 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 onEngine
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 toMySQLDatabase
.- 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 onDatabase
, 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 ofEngine
, but it has an engine.- Using inheritance (
extends Engine
) creates an incorrect "IS-A" relationship instead of a "HAS-A" relationship.
- Using inheritance (
- Unnecessary Method Inheritance: Every method in
Engine
is automatically inherited byCar
, even if it doesn't logically belong to a car.- Example: If
Engine
has a methodchangeOil()
,Car
will inherit it, even though a car itself does not change oilβthe engine does.
- Example: If
- Limited Flexibility & Maintainability: What if we need to support multiple engine types like
PetrolEngine
orElectricEngine
?- 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 anEngine
object and delegates operations to it.
- With inheritance, we'd have to create multiple
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 extendVehicle
. Car
is final, so it cannot be extended further.Truck
is non-sealed, so any class can extend it.Bike
is sealed again, allowing onlyElectricBike
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 onlyPersianCat
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.