Module 1.1

Java Basics & OOP

Master Java programming fundamentals including syntax, data types, variables, operators, object-oriented programming concepts, and the Collections Framework. Build a rock-solid foundation for Data Structures & Algorithms!

45 min read
Beginner
20+ Code Examples
What You'll Learn
  • Java syntax, variables & data types
  • Control flow & operators
  • Classes, objects & inheritance
  • Polymorphism & encapsulation
  • ArrayList, HashMap & Collections
  • JDK, JRE & JVM architecture
Contents
01

Introduction to Java

Java Programming Language

A high-level, class-based, object-oriented programming language designed to have as few implementation dependencies as possible. Java follows the principle of "Write Once, Run Anywhere" (WORA).

Java is one of the most popular programming languages, used for building enterprise applications, Android apps, web services, and solving competitive programming problems.

Fun Fact: Java was originally called "Oak" after an oak tree outside James Gosling's office at Sun Microsystems. It was renamed to Java (after Java coffee) in 1995.

Why Learn Java for DSA?

Java is the preferred language for learning Data Structures and Algorithms:

Strong Typing

  • Catch errors at compile time
  • Clear data type declarations
  • Better code readability
  • Excellent IDE support
  • Auto-completion & refactoring

Rich Standard Library

  • Collections Framework
  • Built-in data structures
  • Sorting algorithms
  • Utility methods
  • Stream API for processing

Industry Standard

  • #1 for coding interviews
  • Enterprise applications
  • Android development
  • Backend systems
  • Big data (Hadoop, Spark)

JDK, JRE, and JVM Architecture

Understanding the Java ecosystem is essential before writing code:

JDK

Java Development Kit

Complete development environment including compiler (javac), debugger, documentation tools, and archiver.

  • javac - compiler
  • java - runtime
  • javadoc - documentation
  • jar - archiver

JRE

Java Runtime Environment

Provides class libraries and JVM to run Java applications. Users who only run Java apps need JRE.

  • JVM - virtual machine
  • Class Libraries
  • Loader - class loader
  • Runtime environment

JVM

Java Virtual Machine

Executes Java bytecode. Provides platform independence - same bytecode runs on any OS with a JVM.

  • Bytecode execution
  • JIT - just-in-time compiler
  • GC - garbage collection
  • Platform independent
Relationship: JDK ⊃ JRE ⊃ JVM. The JDK contains the JRE, which contains the JVM. For development, you need the JDK. To just run Java programs, the JRE is sufficient.
02

Environment Setup

Follow these steps to set up Java on your system:

Windows Installation

1
Download JDK

Download the latest LTS version (JDK 21) from one of these sources:

2
Run the Installer

Double-click the downloaded .exe file and follow the installation wizard

⚠️ Critical: Check "Add to PATH" during installation! This is essential.
3
Verify Installation

Open Command Prompt (search "cmd" in Start Menu) and run:

java --version
javac --version

You should see JDK 21.x or later

macOS Installation

1
Download JDK

Download JDK 21 for macOS (Intel or Apple Silicon M1/M2/M3) from Adoptium or Oracle

2
Run the Installer

Double-click the .dmg file and follow the installation wizard

3
Verify Installation

Open Terminal (Applications → Utilities → Terminal) and run:

java --version
javac --version

You should see JDK 21.x or later

Linux Installation

1
Install via Package Manager

Install Java via your distribution's package manager:

Ubuntu/Debian:

sudo apt update
sudo apt install openjdk-21-jdk

Fedora:

sudo dnf install java-21-openjdk java-21-openjdk-devel
2
Set JAVA_HOME

Add to ~/.bashrc or ~/.zshrc:

export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64
export PATH=$JAVA_HOME/bin:$PATH

Then run: source ~/.bashrc or source ~/.zshrc

3
Verify Installation

Open Terminal and run:

java --version
javac --version

You should see JDK 21.x or later

Choose an IDE

IntelliJ IDEA

Best for Java development. Community Edition is free.

Download
VS Code

Lightweight with Java Extension Pack.

Download
Eclipse

Classic Java IDE, fully open-source.

Download
03

Java Syntax & Structure

Every Java program follows a specific structure. Let's understand the anatomy of a Java program:

Your First Java Program

// HelloWorld.java - File name must match class name

/**
 * This is a Javadoc comment
 * Used for documentation
 */
public class HelloWorld {
    
    // Main method - entry point of the program
    public static void main(String[] args) {
        // Print message to console
        System.out.println("Hello, World!");
        
        // Variables
        String name = "Java Developer";
        int age = 25;
        
        // Print with formatting
        System.out.println("Name: " + name);
        System.out.println("Age: " + age);
        
        // Using printf for formatted output
        System.out.printf("I am %s, %d years old%n", name, age);
    }
}

Key Components Explained

Class Declaration
public class HelloWorld
  • public - Access modifier, visible from anywhere
  • class - Keyword to define a class
  • HelloWorld - Class name (PascalCase)
  • File name must match class name!
Main Method
public static void main(String[] args)
  • public - Accessible by JVM
  • static - Can be called without creating object
  • void - Returns nothing
  • String[] args - Command line arguments

Compiling and Running

# Step 1: Compile (creates HelloWorld.class bytecode)
javac HelloWorld.java

# Step 2: Run (JVM executes the bytecode)
java HelloWorld

# Output:
# Hello, World!
# Name: Java Developer
# Age: 25
# I am Java Developer, 25 years old

# With command line arguments
java HelloWorld arg1 arg2 arg3

Java Naming Conventions

Element Convention Example
Classes PascalCase MyClass, LinkedList, BinarySearchTree
Methods camelCase calculateSum(), findMax(), isEmpty()
Variables camelCase studentName, totalCount, isValid
Constants UPPER_SNAKE_CASE MAX_SIZE, PI, DEFAULT_VALUE
Packages lowercase java.util, com.company.project
04

Data Types in Java

Java is a statically-typed language. Every variable must have a declared type. There are two categories: primitive types and reference types.

Primitive Data Types (8 Types)

Type Size Range Default Example
Integer Types
byte 1 byte (8 bits) -128 to 127 0 byte b = 100;
short 2 bytes (16 bits) -32,768 to 32,767 0 short s = 10000;
int 4 bytes (32 bits) -2³¹ to 2³¹-1 0 int i = 100000;
long 8 bytes (64 bits) -2⁶³ to 2⁶³-1 0L long l = 100000L;
Floating-Point Types
float 4 bytes (32 bits) ~6-7 decimal digits 0.0f float f = 3.14f;
double 8 bytes (64 bits) ~15 decimal digits 0.0d double d = 3.14159;
Other Types
char 2 bytes (16 bits) 0 to 65,535 (Unicode) '\u0000' char c = 'A';
boolean 1 bit true or false false boolean b = true;

Reference Types

Reference types store references (memory addresses) to objects on the heap:

// String - Most commonly used reference type
String name = "Java";           // String literal (String pool)
String name2 = new String("Java"); // New object on heap

// Arrays - Fixed-size collections
int[] numbers = {1, 2, 3, 4, 5};     // Array literal
int[] arr = new int[5];               // Array with size
String[] names = {"Alice", "Bob"};    // String array

// Wrapper Classes (autoboxing/unboxing)
Integer boxedInt = 100;        // Autoboxing: int → Integer
int primitiveInt = boxedInt;   // Unboxing: Integer → int

// All wrapper classes
Byte, Short, Integer, Long, Float, Double, Character, Boolean

// Null reference
String str = null;  // Reference types can be null
// int num = null;  // Compile error! Primitives cannot be null
Memory Tip for DSA: Primitive types are stored on the stack (fast access), while reference types point to objects on the heap. Understanding this is crucial for:
  • Analyzing space complexity
  • Understanding pass-by-value vs pass-by-reference
  • Knowing when to use primitives vs wrapper classes

Type Casting

// Widening (Implicit) - No data loss
int myInt = 100;
long myLong = myInt;    // int → long (automatic)
double myDouble = myLong; // long → double (automatic)

// Narrowing (Explicit) - Possible data loss
double d = 9.78;
int i = (int) d;        // d = 9 (decimal part lost)

long l = 1000L;
int x = (int) l;        // May overflow if l > Integer.MAX_VALUE

// Character to int
char ch = 'A';
int ascii = ch;         // ascii = 65 (ASCII value)

// Parse strings to numbers (common in competitive programming)
String numStr = "123";
int num = Integer.parseInt(numStr);
double dbl = Double.parseDouble("3.14");
long lng = Long.parseLong("999999999999");
05

Variables & Constants

Variables are containers for storing data values. Java has strict rules for variable declaration and scope.

Declaring Variables

// Variable declaration and initialization
int age;                    // Declaration only
age = 25;                   // Initialization later

int count = 10;             // Declaration + Initialization (recommended)

// Multiple declarations (same type)
int x, y, z;
int a = 1, b = 2, c = 3;

// Type inference with var (Java 10+)
var message = "Hello";      // Compiler infers String
var number = 42;            // Compiler infers int
var list = new ArrayList(); // Infers ArrayList

// var rules:
// - Must be initialized at declaration
// - Cannot be null initially
// - Only for local variables (not fields)

Constants with final

// Constants - cannot be changed after initialization
final double PI = 3.14159265359;
final int MAX_SIZE = 100;
final String COMPANY_NAME = "ClassNotes";

// PI = 3.14;  // Compile error! Cannot reassign final variable

// Blank final - initialized once, in constructor
class Circle {
    final double radius;  // Blank final
    
    Circle(double r) {
        this.radius = r;  // Must initialize in constructor
    }
}

// Static final - class-level constants
public class Constants {
    public static final int MAX_ARRAY_SIZE = 1000;
    public static final double GOLDEN_RATIO = 1.618033988749;
}

Variable Scope

public class ScopeExample {
    // Instance variable (object scope)
    private int instanceVar = 10;
    
    // Static variable (class scope)
    private static int staticVar = 20;
    
    // Static constant
    public static final int CONSTANT = 100;
    
    public void method(int paramVar) {  // Parameter scope
        // Local variable (method scope)
        int localVar = 30;
        
        // Block scope
        if (true) {
            int blockVar = 40;  // Only inside this if block
            System.out.println(blockVar); // OK
        }
        // System.out.println(blockVar); // Error! Not accessible
        
        // Loop scope
        for (int i = 0; i < 5; i++) {
            int loopVar = i * 2;  // Only inside loop
        }
        // System.out.println(i); // Error! i not accessible
    }
}
Good Practices
  • Initialize variables when declaring
  • Use final for constants
  • Keep scope as small as possible
  • Use descriptive names: studentAge not sa
  • Declare variables close to where they're used
Avoid
  • Starting names with numbers: 1stPlace
  • Using reserved keywords: class, int
  • Single letters (except loops): x, y
  • Abbreviations: usrNm instead of userName
  • Shadowing outer variables unnecessarily
06

Operators in Java

Operators perform operations on variables and values. Java supports several types of operators essential for DSA:

Arithmetic Operators

int a = 10, b = 3;

System.out.println(a + b);   // 13 - Addition
System.out.println(a - b);   // 7  - Subtraction
System.out.println(a * b);   // 30 - Multiplication
System.out.println(a / b);   // 3  - Integer Division (not 3.33!)
System.out.println(a % b);   // 1  - Modulus (remainder)

// For decimal division, use double
double result = (double) a / b;  // 3.3333...

// Increment and Decrement
int x = 5;
System.out.println(x++);     // 5 (post-increment, returns then increments)
System.out.println(x);       // 6
System.out.println(++x);     // 7 (pre-increment, increments then returns)
System.out.println(x--);     // 7 (post-decrement)
System.out.println(--x);     // 5 (pre-decrement)

// Math class for advanced operations
Math.pow(2, 10);    // 1024.0 (2^10)
Math.sqrt(16);      // 4.0
Math.abs(-5);       // 5
Math.max(10, 20);   // 20
Math.min(10, 20);   // 10

Comparison & Logical Operators

int a = 10, b = 20;

// Comparison Operators (return boolean)
System.out.println(a == b);  // false - Equal to
System.out.println(a != b);  // true  - Not equal
System.out.println(a > b);   // false - Greater than
System.out.println(a < b);   // true  - Less than
System.out.println(a >= b);  // false - Greater or equal
System.out.println(a <= b);  // true  - Less or equal

// Logical Operators
boolean x = true, y = false;
System.out.println(x && y);  // false - AND (both must be true)
System.out.println(x || y);  // true  - OR (at least one true)
System.out.println(!x);      // false - NOT (negation)

// Short-circuit evaluation (important for DSA!)
// && stops if first is false, || stops if first is true
int[] arr = null;
// This is safe - second condition not evaluated if arr is null
if (arr != null && arr.length > 0) {
    System.out.println(arr[0]);
}

// Useful pattern for bounds checking
int i = 5, n = 10;
if (i >= 0 && i < n && arr[i] > 0) {
    // Safe array access
}

Bitwise Operators (Essential for DSA!)

int a = 5;  // Binary: 0101
int b = 3;  // Binary: 0011

// Basic bitwise operations
System.out.println(a & b);   // 1  - AND  (0001)
System.out.println(a | b);   // 7  - OR   (0111)
System.out.println(a ^ b);   // 6  - XOR  (0110)
System.out.println(~a);      // -6 - NOT  (inverts all bits)
System.out.println(a << 1);  // 10 - Left shift  (1010) = a * 2
System.out.println(a >> 1);  // 2  - Right shift (0010) = a / 2
System.out.println(a >>> 1); // 2  - Unsigned right shift

//  IMPORTANT DSA PATTERNS 

// Check if number is odd or even
boolean isOdd = (n & 1) == 1;     // Last bit is 1 for odd
boolean isEven = (n & 1) == 0;    // Last bit is 0 for even

// Multiply by 2
int doubled = n << 1;             // n * 2

// Divide by 2
int halved = n >> 1;              // n / 2

// Check if power of 2
boolean isPowerOf2 = (n > 0) && ((n & (n - 1)) == 0);

// Swap two numbers without temp variable
a = a ^ b;
b = a ^ b;
a = a ^ b;

// Get ith bit
int getBit = (n >> i) & 1;

// Set ith bit to 1
int setBit = n | (1 << i);

// Clear ith bit (set to 0)
int clearBit = n & ~(1 << i);

// Toggle ith bit
int toggleBit = n ^ (1 << i);
DSA Tip: Bitwise operators are O(1) and extremely fast! Common uses:
  • Subsets: Generate all 2^n subsets using bit manipulation
  • XOR tricks: Find single number, swap without temp
  • Optimization: Replace division/multiplication with shifts
  • Bit counting: Count set bits (Brian Kernighan's algorithm)

Assignment Operators

int x = 10;

x += 5;   // x = x + 5 = 15
x -= 3;   // x = x - 3 = 12
x *= 2;   // x = x * 2 = 24
x /= 4;   // x = x / 4 = 6
x %= 4;   // x = x % 4 = 2

// Bitwise compound assignment
x &= 3;   // x = x & 3
x |= 4;   // x = x | 4
x ^= 2;   // x = x ^ 2
x <<= 1;  // x = x << 1
x >>= 1;  // x = x >> 1
07

Control Flow Statements

Control flow determines the order of code execution. Mastering these is fundamental for implementing algorithms!

Conditional Statements

Make decisions in your code by executing different blocks based on conditions:

Conditional Statements

if executes code when condition is true. if-else provides alternative path when false. if-else if-else checks multiple conditions. Ternary operator is shorthand for simple if-else. switch selects from multiple cases based on value.

Essential for decision-making in algorithms—used in search algorithms, validation, error handling, and control flow. Proper conditional logic ensures correct algorithm behavior for all input cases.

If Statement - Single Condition

// Execute code only if condition is true
int age = 18;
if (age >= 18) {
    System.out.println("You are an adult");  // Prints
}

If-Else Statement - Two Paths

// One of two paths will execute
int score = 45;
if (score >= 60) {
    System.out.println("Pass");
} else {
    System.out.println("Fail");  // Prints
}

If-Else If-Else - Multiple Conditions

// Check multiple conditions in order
int score = 85;

if (score >= 90) {
    System.out.println("Grade: A");
} else if (score >= 80) {
    System.out.println("Grade: B");  // Prints
} else if (score >= 70) {
    System.out.println("Grade: C");
} else if (score >= 60) {
    System.out.println("Grade: D");
} else {
    System.out.println("Grade: F");
}

Nested If Statements

// If statement inside another if
int age = 20;
int income = 50000;

if (age >= 18) {
    if (income >= 30000) {
        System.out.println("Eligible for loan");  // Prints
    }
}

Ternary Operator - Shorthand If-Else

Concise way to assign values based on condition.

// Format: condition ? valueIfTrue : valueIfFalse
int age = 20;
String status = (age >= 18) ? "Adult" : "Minor";
System.out.println(status);  // Adult

// Find maximum of two numbers
int a = 10, b = 20;
int max = (a > b) ? a : b;  // max = 20

// Chained ternary (use sparingly for readability)
int score = 85;
String grade = (score >= 90) ? "A" :
               (score >= 80) ? "B" :
               (score >= 70) ? "C" : "F";
System.out.println(grade);  // B

Switch Statement - Multiple Cases

Select from multiple cases based on a single value.

// Traditional switch with break
int day = 3;
switch (day) {
    case 1:
        System.out.println("Monday");
        break;  // Don't forget!
    case 2:
        System.out.println("Tuesday");
        break;
    case 3:
        System.out.println("Wednesday");  // Prints
        break;
    case 6:
    case 7:
        System.out.println("Weekend");
        break;
    default:
        System.out.println("Other day");
}

Enhanced Switch Expression (Java 14+)

// Modern switch expression - cleaner syntax
int day = 3;
String dayName = switch (day) {
    case 1 -> "Monday";
    case 2 -> "Tuesday";
    case 3 -> "Wednesday";  // Prints
    case 4 -> "Thursday";
    case 5 -> "Friday";
    case 6, 7 -> "Weekend";  // Multiple values
    default -> "Invalid day";
};

System.out.println(dayName);  // Wednesday

Practical Pattern - Input Validation

// Check multiple conditions with logical operators
int age = 25, score = 85;

if (age >= 18 && score >= 80) {
    System.out.println("Excellent student, adult");
} else if (age >= 18 || score >= 80) {
    System.out.println("One condition met");
} else {
    System.out.println("Both conditions not met");
}

Practical Pattern - Range Checking

// Check if value falls in range
int marks = 75;

if (marks >= 90) {
    System.out.println("Excellent (90-100)");
} else if (marks >= 80) {
    System.out.println("Very Good (80-89)");
} else if (marks >= 70) {
    System.out.println("Good (70-79)");  // Prints
} else if (marks >= 60) {
    System.out.println("Pass (60-69)");
} else {
    System.out.println("Fail (< 60)");
}
When to Use Each
  • if: Single condition check
  • if-else: Two distinct paths
  • if-else if: Multiple ranges/conditions
  • switch: Many discrete values
  • ternary: Quick value assignment
Conditional Best Practices
  • Clear conditions: Make logic obvious
  • Remember break: Essential in switch
  • Avoid nesting: More than 2-3 levels
  • Use logical operators: && (AND), || (OR)
  • Order checks: Most common first
DSA Tip: Conditional logic powers many algorithms:
  • Binary Search: If mid > target, search left; else search right
  • Sorting: Compare elements and swap based on conditions
  • Validation: Check bounds before accessing array/list
  • Termination: Conditions control loop/recursion exit
  • Edge cases: Handle special conditions (null, empty, duplicates)

Loops

Repeat code blocks efficiently with different loop types for various scenarios:

Loop Structures

for loop repeats a fixed number of times. while repeats while condition is true. do-while executes at least once. for-each iterates over arrays/collections. All essential for iterating, searching, and processing data structures.

Loops are the heart of DSA algorithms—used for searching (linear, binary), sorting, graph/tree traversal, and dynamic programming. Choosing the right loop type improves code clarity and algorithm efficiency.

For Loop - Count-Based Iteration

// Basic for loop - iterate n times
for (int i = 0; i < 5; i++) {
    System.out.print(i + " ");  // 0 1 2 3 4
}

For Loop - Reverse Iteration

// Iterate backwards
for (int i = 4; i >= 0; i--) {
    System.out.print(i + " ");  // 4 3 2 1 0
}

// Common in array/list processing from end
int[] arr = {10, 20, 30, 40, 50};
for (int i = arr.length - 1; i >= 0; i--) {
    System.out.print(arr[i] + " ");  // 50 40 30 20 10
}

For Loop - Custom Step/Increment

// Step by 2
for (int i = 0; i < 10; i += 2) {
    System.out.print(i + " ");  // 0 2 4 6 8
}

// Step by 5
for (int i = 0; i <= 20; i += 5) {
    System.out.print(i + " ");  // 0 5 10 15 20
}

// Decrement by 3
for (int i = 20; i > 0; i -= 3) {
    System.out.print(i + " ");  // 20 17 14 11 8 5 2
}

While Loop - Condition-Based Iteration

// While loop - continues while condition is true
int count = 0;
while (count < 5) {
    System.out.print(count + " ");  // 0 1 2 3 4
    count++;
}

// Useful for unknown iterations
int n = 100;
while (n > 1) {
    System.out.print(n + " ");
    n /= 2;  // Keep dividing by 2
}

Do-While Loop - At Least One Execution

// Do-while executes at least once
int n = 0;
do {
    System.out.print(n + " ");  // 0 1 2 3 4
    n++;
} while (n < 5);

// Guaranteed first execution even if condition false
int x = 10;
do {
    System.out.println("Runs once even though x > 5");
} while (x < 5);

For-Each Loop - Iterate Over Collections

Clean syntax for arrays and collections when you don't need index.

// Enhanced for-each - no need to manage index
int[] numbers = {1, 2, 3, 4, 5};
for (int num : numbers) {
    System.out.print(num + " ");  // 1 2 3 4 5
}

// Works with ArrayList too
ArrayList names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
for (String name : names) {
    System.out.println(name);  // Alice, Bob
}

Nested Loops - Multi-Dimensional Iteration

Iterate through 2D arrays, matrices, and nested structures.

// 2D array iteration (matrix)
int[][] matrix = new int[3][3];
for (int i = 0; i < 3; i++) {
    for (int j = 0; j < 3; j++) {
        matrix[i][j] = i * 3 + j;
        System.out.print(matrix[i][j] + " ");
    }
    System.out.println();  // New line after each row
}

// Time complexity: O(rows × cols) = O(n²) for square matrix

Nested Loops - Practical Pattern (Pairs)

// Find all pairs in array
int[] arr = {1, 2, 3, 4};
for (int i = 0; i < arr.length; i++) {
    for (int j = i + 1; j < arr.length; j++) {
        System.out.println("(" + arr[i] + "," + arr[j] + ")");
    }
}

// Output: (1,2) (1,3) (1,4) (2,3) (2,4) (3,4)
// Common pattern for comparing elements

Infinite Loop Patterns

// Pattern 1: while(true)
while (true) {
    // ... do something
    if (condition) break;  // Exit when condition met
}

// Pattern 2: for(;;) - Empty for loop
for (;;) {
    // ... do something
    if (condition) break;  // Exit when condition met
}

// Pattern 3: Practical - User input loop
Scanner scanner = new Scanner(System.in);
while (true) {
    System.out.print("Enter number (0 to exit): ");
    int num = scanner.nextInt();
    if (num == 0) break;
    System.out.println("You entered: " + num);
}
Loop Type Selection
  • For: When iteration count is known
  • While: When condition determines iterations
  • Do-while: Guaranteed at least one execution
  • For-each: Clean iteration over collections
Nested Loop Patterns
  • Matrix: O(rows × cols) time
  • Pairs: Start inner at i+1
  • Triangular: j < i creates upper/lower triangle
  • Graph: Adjacency matrix traversal
DSA Tip: Loops are fundamental to algorithms:
  • Linear Search: For loop through array checking each element
  • Nested Loops: Critical for bubble sort, selection sort, matrix operations
  • While loops: Perfect for "divide and conquer" (binary search)
  • Time Complexity: Single loop = O(n), nested = O(n²), triple = O(n³)
  • Optimization: Understand loop efficiency—often the bottleneck!

Break, Continue & Labels

Control loop flow precisely with break, continue, and labels for nested loop scenarios:

Loop Control Statements

break exits a loop immediately. continue skips current iteration and moves to next. Labels allow controlling outer loops from nested loops, useful for complex multi-level iterations.

Essential for breaking out of nested loops and complex search patterns. Labels enable exiting or continuing from specific outer loops, critical for algorithms like maze solving or matrix search.

Break - Exit Loop Immediately

// Break exits the loop when condition is met
for (int i = 0; i < 10; i++) {
    if (i == 5) {
        break;  // Exit loop when i reaches 5
    }
    System.out.print(i + " ");  // 0 1 2 3 4
}

Continue - Skip Current Iteration

// Continue skips current iteration and goes to next
for (int i = 0; i < 10; i++) {
    if (i % 2 == 0) {
        continue;  // Skip even numbers
    }
    System.out.print(i + " ");  // 1 3 5 7 9 (only odd)
}

Break/Continue in Different Loop Types

// While loop with break
int count = 0;
while (true) {
    if (count == 3) break;  // Exit when count reaches 3
    System.out.print(count + " ");  // 0 1 2
    count++;
}

// Do-while loop with continue
int num = 0;
do {
    num++;
    if (num % 2 == 0) continue;  // Skip even numbers
    System.out.print(num + " ");  // 1 3 5
} while (num < 5);

Labeled Break - Exit from Nested Loops

Exit multiple levels of nested loops using labels (essential for DSA!).

// Label marks the outer loop
outerLoop:
for (int i = 0; i < 3; i++) {
    for (int j = 0; j < 3; j++) {
        // When condition is met, break from OUTER loop
        if (i == 1 && j == 1) {
            System.out.println("Breaking outer loop!");
            break outerLoop;  // Exits both loops
        }
        System.out.print("(" + i + "," + j + ") ");
    }
}

// Output: (0,0) (0,1) (0,2) (1,0) Breaking outer loop!

Labeled Continue - Skip Outer Iteration

Jump to next iteration of outer loop from within nested loop.

// Label marks the outer loop
outerLoop:
for (int i = 0; i < 3; i++) {
    for (int j = 0; j < 3; j++) {
        // When j == 1, skip rest of inner loop and go to next i
        if (j == 1) {
            continue outerLoop;  // Skip to next i iteration
        }
        System.out.print("(" + i + "," + j + ") ");
    }
}

// Output: (0,0) (1,0) (2,0) - only j=0 processed for each i

Practical Pattern - Search in 2D Array

// Find element in 2D matrix and exit immediately
int[][] matrix = {
    {1, 2, 3},
    {4, 5, 6},
    {7, 8, 9}
};

int target = 5;
boolean found = false;

searchLabel:
for (int i = 0; i < matrix.length; i++) {
    for (int j = 0; j < matrix[i].length; j++) {
        if (matrix[i][j] == target) {
            System.out.println("Found " + target + " at position (" + i + "," + j + ")");
            found = true;
            break searchLabel;  // Exit both loops
        }
    }
}

if (!found) {
    System.out.println("Element not found");
}

Practical Pattern - Skip Rows

// Skip entire row if first element is zero
int[][] data = {
    {0, 2, 3},      // Skip this row
    {4, 5, 6},      // Process this row
    {0, 8, 9},      // Skip this row
    {10, 11, 12}    // Process this row
};

rowLoop:
for (int i = 0; i < data.length; i++) {
    if (data[i][0] == 0) {
        continue rowLoop;  // Skip to next row
    }
    
    for (int j = 0; j < data[i].length; j++) {
        System.out.print(data[i][j] + " ");
    }
    System.out.println();
}

// Output:
// 4 5 6
// 10 11 12
Break Use Cases
  • Found target: Exit immediately when element found
  • Condition met: Stop when validation passes/fails
  • Error occurred: Exit loop on error
  • Nested loops: Use labeled break to exit multiple levels
Continue Use Cases
  • Skip invalid: Skip processing invalid data
  • Filter: Process only specific elements
  • Optimization: Avoid unnecessary processing
  • Labeled continue: Skip to outer loop iteration
DSA Tip: Break and continue are critical for:
  • Search algorithms: Break when element found
  • Nested iteration: Control flow through multiple dimensions
  • Matrix operations: Process 2D arrays efficiently
  • Graph traversal: Exit early when goal reached
  • Optimization: Avoid unnecessary iterations
Algorithm Patterns: Different algorithms use different loop patterns:
  • Binary Search: while loop with low/high pointers
  • BFS: while loop with queue
  • DFS: Recursion or while loop with stack
  • Two Pointers: while loop with left/right indices
  • Sliding Window: for loop with variable window
  • Dynamic Programming: Nested for loops for tabulation
08

Object-Oriented Programming (OOP)

Java is fundamentally an object-oriented language. Understanding OOP is essential for implementing data structures and designing clean, reusable code.

The Four Pillars of OOP

Encapsulation

Bundling data and methods that operate on that data within a single unit (class), hiding internal details.

private fields + public getters/setters

Inheritance

A class can inherit properties and methods from another class, promoting code reuse.

class Child extends Parent

Polymorphism

Objects of different classes can be treated as objects of a common parent class.

Method overriding & overloading

Abstraction

Hiding complex implementation details and showing only necessary features.

abstract class & interface

Classes and Objects

Build blueprints for creating objects and managing data with methods:

Class & Object

A class is a blueprint/template that defines properties (variables) and behaviors (methods). An object is an instance of a class - an actual copy with specific values.

Classes encapsulate data and methods together. Objects are created from classes using the new keyword. Each object has its own state (instance variables) but shares behavior (methods) with other objects of the same class.

Class Definition with Fields

// Class definition - blueprint for Student objects
public class Student {
    // Instance variables (fields) - unique for each object
    private String name;
    private int age;
    private double gpa;
    
    // Static variable - shared across ALL instances
    private static int studentCount = 0;
}

Constructors - Create Objects

// No-argument constructor (default)
public Student() {
    this.name = "Unknown";
    this.age = 0;
    this.gpa = 0.0;
    studentCount++;
}

// Parameterized constructor - initialize with values
public Student(String name, int age, double gpa) {
    this.name = name;
    this.age = age;
    this.gpa = gpa;
    studentCount++;  // Increment count each time object created
}

Getters - Read Property Values

// Getter methods - retrieve values safely
public String getName() {
    return name;
}

public int getAge() {
    return age;
}

public double getGpa() {
    return gpa;
}

Setters - Modify Property Values

// Setter methods - update values with validation
public void setName(String name) {
    this.name = name;
}

public void setAge(int age) {
    // Validate: age must be positive
    if (age > 0) {
        this.age = age;
    }
}

public void setGpa(double gpa) {
    // Validate: GPA between 0 and 4.0
    if (gpa >= 0 && gpa <= 4.0) {
        this.gpa = gpa;
    }
}

Instance Methods - Behavior

// Instance methods - operate on object data
public void displayInfo() {
    System.out.println("Name: " + name);
    System.out.println("Age: " + age);
    System.out.println("GPA: " + gpa);
}

public boolean isPassed() {
    return gpa >= 2.0;  // Passed if GPA >= 2.0
}

public String getGrade() {
    if (gpa >= 3.5) return "A";
    if (gpa >= 3.0) return "B";
    if (gpa >= 2.0) return "C";
    return "F";
}

Static Methods - Class-Level Behavior

// Static method - belongs to class, not objects
public static int getStudentCount() {
    return studentCount;
}

public static void resetCount() {
    studentCount = 0;
}

// Note: Static methods can ONLY access static variables
// They cannot access instance variables (name, age, gpa)

Override toString() Method

// Override toString() for better string representation
@Override
public String toString() {
    return "Student{" +
           "name='" + name + '\'' +
           ", age=" + age +
           ", gpa=" + gpa +
           '}';
}

// Usage:
Student s = new Student("Alice", 20, 3.8);
System.out.println(s);  // Student{name='Alice', age=20, gpa=3.8}

Creating and Using Objects

// Create objects using constructors
Student s1 = new Student();                      // Default constructor
Student s2 = new Student("Alice", 20, 3.8);      // Parameterized constructor
Student s3 = new Student("Bob", 22, 3.2);

// Use setters to modify values
s1.setName("Charlie");
s1.setAge(21);
s1.setGpa(3.5);

// Use getters to retrieve values
String name = s2.getName();     // "Alice"
int age = s2.getAge();          // 20

// Call instance methods
s2.displayInfo();               // Display Alice's info
System.out.println(s2.isPassed());  // true (GPA 3.8 >= 2.0)
System.out.println(s2.getGrade());  // A (GPA 3.8 >= 3.5)

Static Variables & Methods Example

// Demonstrate static variable shared across all objects
Student s1 = new Student("Alice", 20, 3.8);     // studentCount = 1
Student s2 = new Student("Bob", 22, 3.2);       // studentCount = 2
Student s3 = new Student("Charlie", 21, 3.5);   // studentCount = 3

// Static method accesses static variable
System.out.println("Total students: " + Student.getStudentCount());  // 3

// Each object has its own instance variables
System.out.println(s1.getName());   // Alice
System.out.println(s2.getName());   // Bob
System.out.println(s3.getName());   // Charlie

// But they share the static count
System.out.println(Student.getStudentCount());  // Still 3
Instance vs Static
  • Instance Variables: Unique per object, accessed via object
  • Static Variables: Shared by all, accessed via class
  • Instance Methods: Operate on object data, use this
  • Static Methods: Class-level, cannot use instance variables
Encapsulation Best Practices
  • private fields - hide implementation
  • public getters/setters - controlled access
  • Validation in setters - ensure data integrity
  • final fields - immutable values
  • Use meaningful names - self-documenting code
DSA Tip: Classes and objects are fundamental for:
  • TreeNode: Custom class to represent nodes in tree/graph
  • Comparable: Implement compare logic in custom classes
  • LinkedList: Node objects linked together
  • Data organization: Group related data in classes
  • Algorithm design: Encapsulate state and behavior

Inheritance

Extend classes to reuse code and establish relationships between parent and child classes:

Inheritance (IS-A Relationship)

A mechanism where a subclass (child) inherits properties and methods from a superclass (parent). The child can reuse parent's code, override methods, and add new functionality.

Java supports single inheritance only (one parent class). Use inheritance for IS-A relationships (Dog IS-A Animal). Promotes code reuse and establishes class hierarchy.

Parent Class Definition

// Parent/Base/Super class - defines common behavior
class Animal {
    // Protected: accessible by subclasses
    protected String name;
    protected int age;
    
    // Constructor
    public Animal(String name) {
        this.name = name;
        this.age = 0;
    }
    
    // Common method inherited by all animals
    public void eat() {
        System.out.println(name + " is eating");
    }
    
    public void sleep() {
        System.out.println(name + " is sleeping");
    }
    
    // Method that can be overridden
    public void makeSound() {
        System.out.println(name + " makes a sound");
    }
}

Child Class Extending Parent

// Child/Derived/Sub class - extends parent and adds specific behavior
class Dog extends Animal {
    // Child-specific field
    private String breed;
    
    // Constructor must call parent constructor using super()
    public Dog(String name, String breed) {
        super(name);  // Call parent constructor (MUST be first line)
        this.breed = breed;
    }
    
    // Override parent method with dog-specific implementation
    @Override
    public void makeSound() {
        System.out.println(name + " barks: Woof! Woof!");
    }
    
    // Child-specific method
    public void fetchBall() {
        System.out.println(name + " fetches the ball");
    }
    
    public String getBreed() {
        return breed;
    }
}

Another Child Class

// Another subclass of Animal
class Cat extends Animal {
    private boolean isIndoor;
    
    public Cat(String name, boolean isIndoor) {
        super(name);  // Call parent constructor
        this.isIndoor = isIndoor;
    }
    
    // Override makeSound with cat-specific behavior
    @Override
    public void makeSound() {
        System.out.println(name + " meows: Meow! Meow!");
    }
    
    // Cat-specific method
    public void scratch() {
        System.out.println(name + " is scratching the furniture");
    }
    
    public boolean isIndoor() {
        return isIndoor;
    }
}

Inheritance in Action - Creating Objects

// Create subclass instances
Dog dog = new Dog("Buddy", "Golden Retriever");
Cat cat = new Cat("Whiskers", true);

// Access inherited methods from Animal class
dog.eat();              // Buddy is eating (inherited)
dog.sleep();            // Buddy is sleeping (inherited)

cat.eat();              // Whiskers is eating (inherited)
cat.sleep();            // Whiskers is sleeping (inherited)

Method Overriding vs Inheritance

// Call overridden methods - each subclass has own implementation
dog.makeSound();        // Buddy barks: Woof! Woof! (Dog's version)
cat.makeSound();        // Whiskers meows: Meow! Meow! (Cat's version)

// Call child-specific methods
dog.fetchBall();        // Buddy fetches the ball (Dog only)
cat.scratch();          // Whiskers is scratching the furniture (Cat only)

// Access child-specific getters
System.out.println(dog.getBreed());     // Golden Retriever
System.out.println(cat.isIndoor());     // true

Using super() - Calling Parent Method

class Dog extends Animal {
    private String breed;
    
    public Dog(String name, String breed) {
        super(name);  // Call parent constructor to initialize name
        this.breed = breed;
    }
    
    // Override and call parent's method
    @Override
    public void makeSound() {
        super.makeSound();  // Call parent's makeSound()
        System.out.println(name + " specifically barks!");
    }
}

// Usage
Dog dog = new Dog("Buddy", "Labrador");
dog.makeSound();
// Output:
// Buddy makes a sound (from super.makeSound())
// Buddy specifically barks!

Practical Pattern - Hierarchy with Inheritance

// Create different animals
Animal[] animals = {
    new Dog("Max", "Bulldog"),
    new Cat("Mittens", false),
    new Dog("Rex", "Husky"),
    new Cat("Shadow", true)
};

// Process all animals uniformly through parent reference
System.out.println("=== All animals eating ===");
for (Animal animal : animals) {
    animal.eat();  // Works for all subclasses
}

System.out.println("\n=== All animals making sounds ===");
for (Animal animal : animals) {
    animal.makeSound();  // Calls correct subclass method
}

// Output:
// Max is eating, Mittens is eating, etc.
// Max barks: Woof! Woof!
// Mittens meows: Meow! Meow!, etc.
Inheritance Benefits
  • Code Reuse: Inherit methods from parent
  • Hierarchy: Organize classes logically
  • Extensibility: Add new functionality in child
  • Maintenance: Update parent, all children benefit
  • Polymorphism: Same interface, different behavior
Important Keywords
  • extends: Child class inherits from parent
  • super(): Call parent's constructor
  • super.method(): Call parent's method
  • @Override: Annotation for overriding
  • protected: Access modifier for parent use
DSA Tip: Inheritance is crucial for:
  • LinkedList, ArrayList: Both implement List interface via inheritance
  • TreeNode: Different node types (TreeNode, GraphNode) share behavior
  • Custom comparators: Classes implement Comparable through inheritance
  • Generics: Parent class defines type constraints for children
  • Framework design: Base classes define contract, subclasses implement

Polymorphism

One of the four OOP pillars - ability of objects to take multiple forms and methods to behave differently:

Polymorphism ("Many Forms")

The ability of a method to behave differently based on the object calling it. Two types: Compile-time (Method Overloading) and Runtime (Method Overriding).

Enables writing flexible, reusable code. Same method name can perform different operations depending on context. Essential for designing extensible systems and implementing design patterns.

Method Overloading (Compile-time Polymorphism)

Same method name with different parameters. Compiler decides which to call.

class Calculator {
    // Overload 1: Add two integers
    public int add(int a, int b) {
        return a + b;
    }
    
    // Overload 2: Add three integers (different parameters)
    public int add(int a, int b, int c) {
        return a + b + c;
    }
    
    // Overload 3: Add two doubles (different type)
    public double add(double a, double b) {
        return a + b;
    }
    
    // Overload 4: Add int and double (different types)
    public double add(int a, double b) {
        return a + b;
    }
}

Using Overloaded Methods

Calculator calc = new Calculator();

// Compiler determines which add() to call based on parameters
int result1 = calc.add(5, 10);              // Calls add(int, int) → 15
int result2 = calc.add(5, 10, 15);          // Calls add(int, int, int) → 30
double result3 = calc.add(5.5, 10.5);       // Calls add(double, double) → 16.0
double result4 = calc.add(5, 10.5);         // Calls add(int, double) → 15.5
Overloading Rules: Different parameter list (count, type, or order). Return type alone is NOT enough!

Method Overriding (Runtime Polymorphism)

Subclass provides specific implementation of parent's method. Actual method called determined at runtime.

// Parent class
class Shape {
    public double area() {
        System.out.println("Shape area calculation");
        return 0;
    }
    
    public void display() {
        System.out.println("This is a shape");
    }
}

// Subclass 1
class Circle extends Shape {
    private double radius;
    
    public Circle(double radius) {
        this.radius = radius;
    }
    
    // Override parent's area method
    @Override
    public double area() {
        return Math.PI * radius * radius;
    }
    
    // Override parent's display method
    @Override
    public void display() {
        System.out.println("Circle with radius: " + radius);
    }
}

// Subclass 2
class Rectangle extends Shape {
    private double width, height;
    
    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }
    
    @Override
    public double area() {
        return width * height;
    }
    
    @Override
    public void display() {
        System.out.println("Rectangle: " + width + " x " + height);
    }
}

Polymorphic Behavior - Method Called at Runtime

// Parent reference can hold any subclass object
Shape shape1 = new Circle(5);
Shape shape2 = new Rectangle(4, 6);

// Runtime decides which area() to call based on actual object
System.out.println("Circle area: " + shape1.area());      // 78.53 (Circle's area())
System.out.println("Rectangle area: " + shape2.area());   // 24.0 (Rectangle's area())

// Display method
shape1.display();   // Circle with radius: 5
shape2.display();   // Rectangle: 4.0 x 6.0

Practical Pattern - Array of Different Shapes

// Store different shapes in one array
Shape[] shapes = {
    new Circle(5),
    new Rectangle(4, 6),
    new Circle(3),
    new Rectangle(2, 8)
};

// Calculate total area - polymorphism handles different calculations
double totalArea = 0;
for (Shape shape : shapes) {
    totalArea += shape.area();  // Calls correct area() method
    shape.display();             // Calls correct display() method
}

System.out.println("Total area: " + totalArea);

// Output:
// Circle with radius: 5
// Rectangle: 4.0 x 6.0
// Circle with radius: 3
// Rectangle: 2.0 x 8.0
// Total area: 134.39...
Method Overloading
  • When: Compile time
  • How: Different parameters
  • Same class: Can be in same/parent class
  • Return type: Can differ (not required)
  • Example: + operator (int + int, string + string)
Method Overriding
  • When: Runtime
  • How: Same signature in subclass
  • Inheritance: Must be in parent-child
  • Access: Same or more public
  • Annotation: Use @Override
DSA Tip: Polymorphism is crucial for:
  • Data structures: ArrayList, HashMap work with any type
  • Sorting: Comparable interface defines compareTo() for any class
  • Graph algorithms: Process different Node types uniformly
  • Strategy pattern: Different algorithms through same interface
  • Flexible code: Add new shapes without changing existing code

Abstract Classes & Interfaces

Define contracts and blueprints for classes to ensure consistency and structure:

Abstract Classes

A class that cannot be instantiated directly and serves as a template for subclasses. Contains both abstract methods (no implementation) and concrete methods (with implementation).

Use when subclasses share common code (concrete methods) and behavior (abstract methods). Can have constructors, instance variables, and access modifiers. Single inheritance only.

Abstract Class Definition

// Abstract class - cannot be instantiated
abstract class Vehicle {
    // Instance variable
    protected String brand;
    
    // Constructor (must be called by subclass)
    public Vehicle(String brand) {
        this.brand = brand;
    }
    
    // Abstract method - no implementation, must override
    public abstract void start();
    
    // Abstract method with different purpose
    public abstract void stop();
    
    // Concrete method - has implementation, can be inherited
    public void honk() {
        System.out.println(brand + " honking!");
    }
}

Implementing Abstract Class

// Concrete subclass must implement ALL abstract methods
class Car extends Vehicle {
    private String fuelType;
    
    public Car(String brand, String fuelType) {
        super(brand);  // Call parent constructor
        this.fuelType = fuelType;
    }
    
    // Must override abstract method start()
    @Override
    public void start() {
        System.out.println(brand + " car started with key (" + fuelType + ")");
    }
    
    // Must override abstract method stop()
    @Override
    public void stop() {
        System.out.println(brand + " car engine stopped");
    }
    
    // Inherited concrete method (optional to override)
    // Inherited: honk() from Vehicle
}

// Another implementation
class Bike extends Vehicle {
    @Override
    public void start() {
        System.out.println(brand + " bike started with button");
    }
    
    @Override
    public void stop() {
        System.out.println(brand + " bike engine stopped");
    }
}

Using Abstract Classes Polymorphically

// Create subclass instances and use through parent reference
Vehicle car = new Car("Toyota", "Petrol");
Vehicle bike = new Bike("Hero");

// Call abstract methods - actual implementation depends on object type
car.start();    // Output: Toyota car started with key (Petrol)
bike.start();   // Output: Hero bike started with button

// Call concrete inherited method
car.honk();     // Output: Toyota honking!
bike.honk();    // Output: Hero honking!

Interfaces

A contract/specification that defines what a class can do, but not how. Contains only abstract methods (Java 7) or can have default/static methods (Java 8+).

Use to define behavior that unrelated classes should have. A class can implement multiple interfaces but extend only one class. No constructors, no instance variables.

Interface Definition (Basic)

// Interface - contract for behavior
interface Animal {
    // Abstract methods (no body) - must be implemented
    void eat();
    void sleep();
    
    // Method with implementation (Java 8+)
    void run();
}

interface Swimmer {
    void swim();
    void dive();
}

interface Flyer {
    void fly();
}

Implementing Single Interface

// Class implements one interface
class Dog implements Animal {
    @Override
    public void eat() {
        System.out.println("Dog eating meat");
    }
    
    @Override
    public void sleep() {
        System.out.println("Dog sleeping");
    }
    
    @Override
    public void run() {
        System.out.println("Dog running fast");
    }
}

// Usage
Animal dog = new Dog();
dog.eat();      // Dog eating meat
dog.sleep();    // Dog sleeping
dog.run();      // Dog running fast

Implementing Multiple Interfaces

// Class implements multiple interfaces
class Duck implements Animal, Swimmer, Flyer {
    @Override
    public void eat() {
        System.out.println("Duck eating grains");
    }
    
    @Override
    public void sleep() {
        System.out.println("Duck sleeping");
    }
    
    @Override
    public void run() {
        System.out.println("Duck running");
    }
    
    @Override
    public void swim() {
        System.out.println("Duck swimming in water");
    }
    
    @Override
    public void dive() {
        System.out.println("Duck diving underwater");
    }
    
    @Override
    public void fly() {
        System.out.println("Duck flying in sky");
    }
}

// Can use through any interface reference
Duck duck = new Duck();
duck.eat();         // Duck eating grains
duck.swim();        // Duck swimming in water
duck.fly();         // Duck flying in sky

Default and Static Methods (Java 8+)

// Interface with default and static methods
interface Drawable {
    // Abstract method
    void draw();
    
    // Default method - has implementation, can be overridden
    default void print() {
        System.out.println("Printing drawable object");
    }
    
    // Static method - belongs to interface, cannot be overridden
    static void info() {
        System.out.println("This is a Drawable interface");
    }
}

// Implementation
class Circle implements Drawable {
    @Override
    public void draw() {
        System.out.println("Drawing circle");
    }
    
    // Optional: can override default method
    @Override
    public void print() {
        System.out.println("Printing circle with custom format");
    }
}

// Usage
Circle circle = new Circle();
circle.draw();           // Drawing circle
circle.print();          // Printing circle with custom format
Drawable.info();         // This is a Drawable interface

Real-World Pattern - Comparable Interface

// Built-in Comparable interface for sorting
interface Comparable {
    int compareTo(T other);  // Returns: negative if <, 0 if ==, positive if >
}

// Implementation for Student
class Student implements Comparable {
    String name;
    int marks;
    
    Student(String name, int marks) {
        this.name = name;
        this.marks = marks;
    }
    
    @Override
    public int compareTo(Student other) {
        // Sort by marks descending (highest first)
        return other.marks - this.marks;
    }
    
    @Override
    public String toString() {
        return name + " (" + marks + ")";
    }
}

// Usage with Collections.sort()
ArrayList students = new ArrayList<>();
students.add(new Student("Alice", 95));
students.add(new Student("Bob", 87));
students.add(new Student("Charlie", 92));

Collections.sort(students);  // Sorts by compareTo() method
// Output: Alice (95), Charlie (92), Bob (87)
Abstract Class vs Interface
  • Abstract Class: Share code (inheritance)
  • Interface: Define behavior contract
  • Inheritance: Class → one parent only
  • Implementation: Class → multiple interfaces
  • Variables: Abstract has instance vars, Interface doesn't
  • Constructor: Abstract has, Interface doesn't
When to Use
  • Abstract Class: Classes are closely related (is-a relationship)
  • Abstract Class: Need shared state or methods
  • Interface: Unrelated classes should behave similarly
  • Interface: Define behavior contract only
  • Interface: Multiple inheritance needed
DSA Tip: Abstract classes and interfaces are crucial for:
  • Data structures: Define common interface (List, Set, Map)
  • Custom comparators: Implement Comparator interface for sorting
  • Graph problems: Define Node interface for graph algorithms
  • Design patterns: Strategy, Observer patterns use interfaces
  • Code flexibility: Switch implementations without changing client code
09

Collections Framework

The Java Collections Framework provides ready-to-use data structures essential for DSA. Understanding these is crucial for efficient problem-solving!

Collections Hierarchy

Interface Implementation Use Case Time Complexity
List - Ordered, allows duplicates
List ArrayList Dynamic array, fast random access get: O(1), add: O(1)*, remove: O(n)
LinkedList Doubly linked list, fast insert/delete get: O(n), add: O(1), remove: O(1)
Set - No duplicates
Set HashSet Fast lookup, no order add/remove/contains: O(1)
LinkedHashSet Maintains insertion order add/remove/contains: O(1)
TreeSet Sorted order add/remove/contains: O(log n)
Map - Key-Value pairs
Map HashMap Fast lookup by key put/get/remove: O(1)
LinkedHashMap Maintains insertion order put/get/remove: O(1)
TreeMap Sorted by keys put/get/remove: O(log n)
Queue/Deque - FIFO/LIFO operations
Queue PriorityQueue Min/Max heap offer: O(log n), poll: O(log n), peek: O(1)
Deque ArrayDeque Stack/Queue operations all operations: O(1)

ArrayList - Dynamic Array

A resizable array that grows automatically when needed, perfect for dynamic collections:

ArrayList

A resizable array implementation that grows dynamically as you add elements. Provides fast random access O(1) by index, but slower insertion/deletion in the middle O(n).

Unlike arrays, ArrayLists automatically expand when full. Use ArrayList when you need frequent random access. Perfect for: storing dynamic collections, building lists, caching results.

ArrayList Creation

import java.util.ArrayList;

// Creating ArrayList with different types
ArrayList list = new ArrayList<>();        // Empty, default capacity
ArrayList names = new ArrayList<>(100);     // Initial capacity 100
ArrayList prices = new ArrayList<>();       // Type specific

// Check size and capacity
int size = list.size();                             // Number of elements

Adding Elements

ArrayList list = new ArrayList<>();

// Add at the end
list.add(10);           // [10]
list.add(20);           // [10, 20]
list.add(30);           // [10, 20, 30]

// Add at specific index (shifts elements right)
list.add(1, 15);        // [10, 15, 20, 30] - insert 15 at index 1

// Add all elements from another collection
list.addAll(Arrays.asList(40, 50));  // [10, 15, 20, 30, 40, 50]

// Add at specific position from another list
list.addAll(2, Arrays.asList(17, 18));  // [10, 15, 17, 18, 20, 30, 40, 50]

Accessing Elements

ArrayList list = new ArrayList<>(Arrays.asList(10, 15, 20, 30));

// Get element by index
int first = list.get(0);                    // 10
int second = list.get(1);                   // 15

// Get last element
int last = list.get(list.size() - 1);       // 30

// Safely get with default fallback
Integer value = (list.size() > 5) ? list.get(5) : -1;  // Safe access

Modifying Elements

ArrayList list = new ArrayList<>(Arrays.asList(10, 15, 20, 30));

// Replace element at index
list.set(0, 100);                   // [100, 15, 20, 30]
list.set(2, 25);                    // [100, 15, 25, 30]

// Check if modification worked
boolean success = list.get(0) == 100;  // true

Removing Elements

ArrayList list = new ArrayList<>(Arrays.asList(10, 15, 20, 30, 20));

// Remove by index
list.remove(0);                     // Removes 10, shifts remaining left

// Remove by value (removes first occurrence)
list.remove(Integer.valueOf(20));   // Removes first 20

// Remove all occurrences
list.removeAll(Arrays.asList(20));  // Removes all 20s

// Remove all elements
list.clear();                       // ArrayList is now empty

Searching & Checking

ArrayList list = new ArrayList<>(Arrays.asList(10, 15, 20, 30, 20));

// Check if element exists
boolean has20 = list.contains(20);           // true
boolean has100 = list.contains(100);         // false

// Find first occurrence
int firstIndex = list.indexOf(20);           // 2 (first 20 is at index 2)

// Find last occurrence
int lastIndex = list.lastIndexOf(20);        // 4 (last 20 is at index 4)

// Check if empty
boolean isEmpty = list.isEmpty();            // false

Iterating Over ArrayList

ArrayList list = new ArrayList<>(Arrays.asList(10, 15, 20, 30));

// Iteration using index (for loop)
for (int i = 0; i < list.size(); i++) {
    System.out.println(list.get(i));  // 10, 15, 20, 30
}

// Enhanced for-each loop
for (int num : list) {
    System.out.println(num);          // 10, 15, 20, 30
}

// forEach with lambda (Java 8+)
list.forEach(num -> System.out.println(num));

// Iterator (useful for removing during iteration)
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
    System.out.println(iterator.next());
}

Sorting ArrayList

import java.util.Collections;

ArrayList list = new ArrayList<>(Arrays.asList(30, 10, 20, 15));

// Sort ascending (smallest to largest)
Collections.sort(list);                     // [10, 15, 20, 30]

// Sort descending (largest to smallest)
Collections.sort(list, Collections.reverseOrder());  // [30, 20, 15, 10]

// Sort with custom comparator
Collections.sort(list, (a, b) -> b - a);   // Descending (alternative)

Converting ArrayList to Array

ArrayList list = new ArrayList<>(Arrays.asList(10, 15, 20, 30));

// Convert to array
Integer[] arr = list.toArray(new Integer[0]);  // [10, 15, 20, 30]

// Convert from array to ArrayList
ArrayList fromArr = new ArrayList<>(Arrays.asList(1, 2, 3, 4));

// Convert to primitive array (for int, double, etc.)
Integer[] boxedArr = list.toArray(new Integer[list.size()]);
int[] primitiveArr = list.stream().mapToInt(Integer::intValue).toArray();

Common Pattern - Count Occurrences

// Count occurrences of each element
ArrayList fruits = new ArrayList<>(
    Arrays.asList("apple", "banana", "apple", "cherry", "apple")
);

HashMap count = new HashMap<>();
for (String fruit : fruits) {
    count.put(fruit, count.getOrDefault(fruit, 0) + 1);
}

// Result: {apple=3, banana=1, cherry=1}
System.out.println(count);
When to Use ArrayList
  • Frequent random access: Get elements by index
  • Dynamic size needed: Size changes at runtime
  • Add/remove at end: O(1) amortized
  • Sorting required: Easy with Collections.sort()
Time Complexities
  • get(): O(1) - direct index access
  • add() at end: O(1) amortized
  • add()/remove() in middle: O(n) - shift elements
  • contains(): O(n) - linear search
  • sort(): O(n log n) - Collections.sort()
DSA Tip: ArrayList is your default choice for:
  • Building results: Store answers while solving
  • Level-order traversal: Store nodes per level in trees
  • Dynamic programming: Store intermediate results
  • Flexible input: Accept variable number of elements
  • Prototyping: Don't know size beforehand? Use ArrayList!

HashMap - Key-Value Storage

Store and retrieve data using unique keys for ultra-fast lookups:

HashMap

A key-value data structure that maps unique keys to values. Each key maps to exactly one value. Uses hashing for O(1) average-case lookups, insertions, and deletions.

HashMaps allow null keys and values (unlike some maps). When you need fast lookup by a unique identifier or frequency counting, HashMap is your go-to choice. Critical for: caching, counters, grouping, deduplication.

HashMap Creation & Adding Entries

import java.util.HashMap;
import java.util.Map;

// Creating HashMap with different type combinations
HashMap scores = new HashMap<>();
Map idToName = new HashMap<>();
HashMap priceMap = new HashMap<>();

// Adding entries with put()
scores.put("Alice", 95);
scores.put("Bob", 87);
scores.put("Charlie", 92);
// HashMap: {Alice=95, Bob=87, Charlie=92}

Retrieving Values

// Get value by key
int aliceScore = scores.get("Alice");        // 95
Integer bobScore = scores.get("Bob");        // 87

// Get with default value (safer - no null exception)
int davidScore = scores.getOrDefault("David", 0);  // 0 (key not found)

// Check if key exists
boolean hasAlice = scores.containsKey("Alice");    // true
boolean hasDavid = scores.containsKey("David");    // false

// Check if value exists
boolean has95 = scores.containsValue(95);         // true
boolean has100 = scores.containsValue(100);       // false

Updating & Conditional Operations

// Update existing key
scores.put("Alice", 98);  // Overwrites old value (95 → 98)

// Put only if key doesn't exist
scores.putIfAbsent("Alice", 100);  // Does nothing (Alice exists)
scores.putIfAbsent("Diana", 88);   // Adds new entry

// Replace operations
scores.replace("Bob", 90);              // Replace only if key exists
scores.replace("Bob", 87, 90);          // Replace only if old value matches

Frequency Counting Pattern (Most Common DSA Use!)

// Count character frequencies in a string
String s = "hello";
HashMap freq = new HashMap<>();

for (char c : s.toCharArray()) {
    freq.put(c, freq.getOrDefault(c, 0) + 1);
}
// Result: {h=1, e=1, l=2, o=1}

// Count word frequencies
String[] words = {"apple", "banana", "apple", "cherry", "apple"};
HashMap wordCount = new HashMap<>();

for (String word : words) {
    wordCount.put(word, wordCount.getOrDefault(word, 0) + 1);
}
// Result: {apple=3, banana=1, cherry=1}

Iterating Over HashMap

HashMap scores = new HashMap<>();
scores.put("Alice", 95);
scores.put("Bob", 87);
scores.put("Charlie", 92);

// Iterate over keys only
for (String key : scores.keySet()) {
    System.out.println(key);  // Alice, Bob, Charlie
}

// Iterate over values only
for (Integer value : scores.values()) {
    System.out.println(value);  // 95, 87, 92
}

Iterating with Key-Value Pairs

// Using entrySet() - Most efficient for key-value pairs
for (Map.Entry entry : scores.entrySet()) {
    String key = entry.getKey();
    Integer value = entry.getValue();
    System.out.println(key + ": " + value);
}
// Output: Alice: 95, Bob: 87, Charlie: 92

// Using forEach with lambda (Java 8+)
scores.forEach((key, value) -> {
    System.out.println(key + ": " + value);
});

Removing & Clearing

// Remove by key
scores.remove("Bob");                    // Removes Bob entry

// Remove only if specific value matches
scores.remove("Alice", 95);              // Removes only if value is 95
scores.remove("Alice", 98);              // Does nothing (value is 98, not 95)

// Check size
int totalEntries = scores.size();        // Number of key-value pairs

// Remove all entries
scores.clear();                          // HashMap is now empty

Common Pattern - Two Sum with HashMap

// Find two numbers that add up to target
public int[] twoSum(int[] nums, int target) {
    HashMap map = new HashMap<>();  // number -> index
    
    for (int i = 0; i < nums.length; i++) {
        int complement = target - nums[i];
        
        if (map.containsKey(complement)) {
            return new int[]{map.get(complement), i};  // Found pair!
        }
        
        map.put(nums[i], i);  // Store for future lookup
    }
    
    return new int[]{};  // No pair found
}

// Example: nums = [2, 7, 11, 15], target = 9
// Result: [0, 1] (nums[0] + nums[1] = 2 + 7 = 9)
HashMap vs TreeMap
  • HashMap: O(1) average, unordered, allows null keys
  • TreeMap: O(log n), sorted by keys, no null keys, maintains order
  • Use HashMap: When you need speed
  • Use TreeMap: When you need sorted order
Time Complexities
  • put(): O(1) average, O(n) worst
  • get(): O(1) average, O(n) worst
  • remove(): O(1) average, O(n) worst
  • containsKey(): O(1) average
  • Space: O(n) where n = entries
DSA Tip: HashMap is one of the most important tools in DSA:
  • Frequency counting: Count occurrences of elements
  • Caching: Store computed results to avoid recalculation
  • Mapping: Map values to indices for quick lookup
  • Grouping: Group elements by a key (anagrams, etc.)
  • Deduplication: Track seen elements to find duplicates

HashSet - Unique Elements

Store unique elements without duplicates with O(1) average lookup time:

HashSet - Unordered Collection

A HashSet stores unique elements without duplicates. It uses a hash table internally for fast operations: O(1) average time for add, remove, and contains. Order is NOT preserved.

Essential for deduplication, membership checking, and finding unique elements. Perfect for "have I seen this before?" problems. Use when uniqueness is required and order doesn't matter. For ordered unique elements, use LinkedHashSet or TreeSet.

HashSet Creation & Adding Elements

import java.util.HashSet;
import java.util.Set;

// Creating HashSet
HashSet set = new HashSet<>();
Set words = new HashSet<>();

// Adding elements
set.add(10);     // set = {10}
set.add(20);     // set = {10, 20}
set.add(10);     // Duplicate - ignored! Still {10, 20}

Checking & Removing Elements

// Checking membership - O(1) average!
boolean has10 = set.contains(10);  // true
boolean has30 = set.contains(30);  // false

// Removing elements - O(1) average
set.remove(10);    // set = {20}
set.clear();       // set = {} (empty)

Iterating Over HashSet

HashSet set = new HashSet<>(Arrays.asList(10, 20, 30));

// For-each loop (no guaranteed order)
for (int num : set) {
    System.out.println(num);  // May print in any order
}

// Using iterator
Iterator iter = set.iterator();
while (iter.hasNext()) {
    System.out.println(iter.next());
}

Set Operations - Union, Intersection, Difference

Combine or compare sets efficiently.

Set set1 = new HashSet<>(Arrays.asList(1, 2, 3, 4));
Set set2 = new HashSet<>(Arrays.asList(3, 4, 5, 6));

// Union - all unique elements from both sets
Set union = new HashSet<>(set1);
union.addAll(set2);  // {1, 2, 3, 4, 5, 6}

// Intersection - common elements
Set intersection = new HashSet<>(set1);
intersection.retainAll(set2);  // {3, 4}

// Difference - elements in set1 but not in set2
Set difference = new HashSet<>(set1);
difference.removeAll(set2);  // {1, 2}

Practical Pattern - Deduplication

Remove duplicates from array or find unique elements.

// Remove duplicates from array
int[] nums = {1, 2, 3, 2, 4, 1, 5, 2};
Set unique = new HashSet<>(Arrays.asList(
    Arrays.stream(nums).boxed().toArray(Integer[]::new)
));
System.out.println(unique);  // {1, 2, 3, 4, 5}

// Simpler way - just create HashSet
Set uniqueSet = new HashSet<>();
for (int num : nums) {
    uniqueSet.add(num);
}

Practical Pattern - Find Duplicates

// Find duplicate elements
int[] nums = {1, 2, 3, 2, 4, 1};
Set seen = new HashSet<>();
Set duplicates = new HashSet<>();

for (int num : nums) {
    if (!seen.add(num)) {  // add() returns false if exists
        duplicates.add(num);
    }
}
System.out.println(duplicates);  // {1, 2}

Practical Pattern - Check if Array Has Duplicates

// Check if array contains any duplicates
boolean hasDuplicates(int[] nums) {
    Set seen = new HashSet<>();
    for (int num : nums) {
        if (!seen.add(num)) {  // If add fails, duplicate found
            return true;
        }
    }
    return false;
}

// Usage
int[] arr1 = {1, 2, 3, 4};       // false (no duplicates)
int[] arr2 = {1, 2, 3, 2, 4};    // true (2 appears twice)

Practical Pattern - First Unique Character

// Find first character that appears only once
String s = "loveleetcode";
Set seen = new HashSet<>();
Set duplicate = new HashSet<>();

for (char c : s.toCharArray()) {
    if (!seen.add(c)) {
        duplicate.add(c);
    }
}

// Find first character not in duplicate set
for (char c : s.toCharArray()) {
    if (!duplicate.contains(c)) {
        System.out.println(c);  // 'v' (first unique)
        break;
    }
}
HashSet Operations
  • add(E): O(1) average, false if duplicate
  • contains(O): O(1) average lookup
  • remove(O): O(1) average
  • size(): O(1) - instant size
  • addAll/retainAll: Batch operations
HashSet vs Other Sets
  • HashSet: Unordered, O(1) ops
  • LinkedHashSet: Maintains insertion order
  • TreeSet: Sorted, O(log n) ops
  • Use HashSet: When only uniqueness matters
DSA Tip: HashSet is essential for many algorithms:
  • Duplicate detection: O(n) time to find all duplicates
  • Membership checking: Much faster than array search
  • Deduplication: Remove duplicates from collections
  • Two-sum pattern: Use HashSet to find pairs efficiently
  • Anagram detection: Compare character sets of strings

Stack, Queue & PriorityQueue

Essential data structures for implementing algorithms and solving problems efficiently:

Stack (LIFO)

A Last-In-First-Out data structure where elements are added and removed from the same end (top). The most recently added element is removed first.

Use ArrayDeque (not the legacy Stack class) for better performance. Common uses: parentheses matching, undo/redo functionality, DFS traversal, expression evaluation.

Stack Creation & Basic Operations

import java.util.Deque;
import java.util.ArrayDeque;

// Creating a Stack (use ArrayDeque, not Stack class)
Deque stack = new ArrayDeque<>();

// Push - add element to top
stack.push(10);    // Stack: [10]
stack.push(20);    // Stack: [20, 10]
stack.push(30);    // Stack: [30, 20, 10]

Stack Retrieval Operations

// Peek - view top element without removing
int top = stack.peek();      // 30 (doesn't remove)

// Pop - remove and return top element
int popped = stack.pop();    // 30 (removes from top)

// Check if empty
boolean isEmpty = stack.isEmpty();  // false

// Get size
int size = stack.size();

Common Stack Pattern - Balanced Parentheses

// Check if parentheses are balanced
public boolean isBalanced(String s) {
    Deque stack = new ArrayDeque<>();
    
    for (char c : s.toCharArray()) {
        if (c == '(' || c == '{' || c == '[') {
            stack.push(c);
        } else {
            if (stack.isEmpty()) return false;
            char top = stack.pop();
            if ((c == ')' && top != '(') ||
                (c == '}' && top != '{') ||
                (c == ']' && top != '[')) {
                return false;
            }
        }
    }
    return stack.isEmpty();
}

Queue (FIFO)

A First-In-First-Out data structure where elements are added at the back and removed from the front. The first element added is the first one removed.

Implement using LinkedList or ArrayDeque. Essential for: BFS traversal, level-order tree traversal, task scheduling, printer queues.

Queue Creation & Basic Operations

import java.util.Queue;
import java.util.LinkedList;

// Creating a Queue
Queue queue = new LinkedList<>();
// Or: Queue queue = new ArrayDeque<>();

// Offer (enqueue) - add element to back
queue.offer(10);   // Queue: [10]
queue.offer(20);   // Queue: [10, 20]
queue.offer(30);   // Queue: [10, 20, 30]

// Add - alternative to offer()
queue.add(40);     // Queue: [10, 20, 30, 40]

Queue Retrieval Operations

// Peek - view front element without removing
int front = queue.peek();    // 10 (doesn't remove)

// Poll (dequeue) - remove and return front element
int removed = queue.poll();  // 10 (removes from front)

// Check if empty
boolean isEmpty = queue.isEmpty();  // false

// Size
int size = queue.size();     // 3

Common Queue Pattern - BFS (Breadth-First Search)

// BFS of binary tree
public void bfs(TreeNode root) {
    if (root == null) return;
    
    Queue queue = new LinkedList<>();
    queue.offer(root);
    
    while (!queue.isEmpty()) {
        TreeNode node = queue.poll();
        System.out.println(node.value);  // Process current node
        
        if (node.left != null) queue.offer(node.left);
        if (node.right != null) queue.offer(node.right);
    }
}

PriorityQueue (Heap)

A data structure where elements are retrieved based on priority rather than insertion order. Implemented as a binary heap. Default is min-heap (smallest element has highest priority).

Operations: offer O(log n), poll/peek O(1). Uses: Dijkstra's algorithm, K-th largest element, task scheduling, event simulation, Huffman coding.

Min Heap (Default)

import java.util.PriorityQueue;

// Min Heap - smallest element has highest priority
PriorityQueue minHeap = new PriorityQueue<>();

// Add elements
minHeap.offer(30);   // Heap: [30]
minHeap.offer(10);   // Heap: [10, 30]
minHeap.offer(20);   // Heap: [10, 30, 20]
minHeap.offer(5);    // Heap: [5, 10, 20, 30]

// View minimum
int min = minHeap.peek();  // 5 (smallest)

// Remove minimum
minHeap.poll();    // Returns 5, next peek() = 10

Max Heap (Using Comparator)

import java.util.Collections;

// Max Heap - largest element has highest priority
PriorityQueue maxHeap = new PriorityQueue<>(Collections.reverseOrder());

// Add elements
maxHeap.offer(30);   // Heap: [30]
maxHeap.offer(10);   // Heap: [30, 10]
maxHeap.offer(20);   // Heap: [30, 20, 10]
maxHeap.offer(50);   // Heap: [50, 30, 20, 10]

// View maximum
int max = maxHeap.peek();  // 50 (largest)

// Remove maximum
maxHeap.poll();    // Returns 50, next peek() = 30

Custom Comparator for Objects

// Custom class
class Student {
    String name;
    int marks;
    
    Student(String name, int marks) {
        this.name = name;
        this.marks = marks;
    }
}

// PriorityQueue with custom comparator (by marks, ascending)
PriorityQueue pq = new PriorityQueue<>(
    (a, b) -> Integer.compare(a.marks, b.marks)
);

pq.offer(new Student("Alice", 95));
pq.offer(new Student("Bob", 87));
pq.offer(new Student("Charlie", 92));

// Retrieve by ascending marks
Student s1 = pq.poll();  // Bob (87)
Student s2 = pq.poll();  // Charlie (92)

// Descending order (reverse comparator)
PriorityQueue pqDesc = new PriorityQueue<>(
    (a, b) -> Integer.compare(b.marks, a.marks)  // b.marks - a.marks
);
// First poll: Alice (95)

Common PriorityQueue Pattern - K Largest Elements

// Find K largest elements using min-heap
public List findKLargest(int[] nums, int k) {
    // Min heap of size k
    PriorityQueue minHeap = new PriorityQueue<>();
    
    for (int num : nums) {
        minHeap.offer(num);
        if (minHeap.size() > k) {
            minHeap.poll();  // Remove smallest if size exceeds k
        }
    }
    
    return new ArrayList<>(minHeap);  // Returns k largest elements
}

// Example: nums = [3, 1, 5, 12, 2, 11], k = 3
// Result: [5, 12, 11]
When to Use
  • Stack: Undo/redo, DFS, parentheses matching, expression evaluation
  • Queue: BFS, level-order traversal, task scheduling, FIFO processing
  • PriorityQueue: Dijkstra's, K-th largest, event simulation, heap sort
Time Complexities
  • Stack: push/pop/peek = O(1)
  • Queue: offer/poll/peek = O(1)
  • PriorityQueue: offer/poll = O(log n), peek = O(1)
DSA Tip: Master these three data structures:
  • Stack: "Last one in, first one out" - like a stack of plates
  • Queue: "First one in, first one out" - like a waiting line
  • PriorityQueue: "Highest priority first" - like VIP queue at a movie theater

Key Takeaways

Platform Independent

Java's "Write Once, Run Anywhere" via JVM makes code portable across all platforms

8 Primitive Types

byte, short, int, long, float, double, char, boolean - know their sizes and ranges!

Strong Typing

Every variable needs a type declaration, catching errors at compile time

OOP Pillars

Encapsulation, Inheritance, Polymorphism, Abstraction - foundation for clean code

Bitwise Operators

Essential for optimization - &, |, ^, ~, <<, >> are used in many DSA problems

Collections Framework

ArrayList, HashMap, HashSet, PriorityQueue - your DSA toolkit for problem-solving

Knowledge Check

Test your understanding of Java Basics & OOP:

Question 1 of 6

What does JVM stand for?

Question 2 of 6

What is the size of an int in Java?

Question 3 of 6

What is the output of 10 / 3 in Java (both operands are int)?

Question 4 of 6

Which keyword is used to declare a constant in Java?

Question 5 of 6

Which OOP concept allows a class to inherit properties from another class?

Question 6 of 6

What is the time complexity of HashMap.get() operation?

Answer all questions to check your score