Java Complete Notes

The ultimate Java reference — from JVM internals and OOP foundations to Collections, Multithreading, Streams API, and real-world patterns. Built for beginners, useful for everyone.
📖 10 Chapters
🎨 Visual Diagrams
💻 50+ Code Examples
🧩 Practice Problems
🎯 LeetCode Links
JVM Architecture
Data Types & Variables
OOP — 4 Pillars
Collections Framework
Exception Handling
Multithreading
Streams & Lambda
File I/O
Generics
Design Patterns
📋 Table of Contents
Navigate through all 10 chapters — 50+ topics covered in depth
Chapter 01
Java Fundamentals
Understand the Java ecosystem — how your .java file transforms into running bytecode across any platform.

🏗️ JVM, JRE, JDK — The Architecture

Java's famous "Write Once, Run Anywhere" (WORA) principle works because code compiles to bytecode, not machine code. The JVM on each platform interprets or JIT-compiles that bytecode.

☕ JDK ⊃ JRE ⊃ JVM — Nested Architecture
📦 JDK (Java Development Kit)
javac compiler + jdb debugger + javadoc + jar tool + ...
📚 JRE (Java Runtime Environment)
Core libraries (java.lang, java.util, java.io ...)
⚙️ JVM (Java Virtual Machine)
ClassLoader → Bytecode Verifier → Interpreter → JIT Compiler → Garbage Collector
ComponentFull NamePurposeWho Needs It?
JDKJava Development KitDevelop + compile + debug + runDevelopers ✅
JREJava Runtime EnvironmentRun compiled Java programsEnd users
JVMJava Virtual MachineExecute bytecode on any OSPart of JRE

⚙️ How Java Code Compiles & Runs

🔄 Compilation → Execution Pipeline
📝 Hello.java
Source Code (human-readable)
⬇️ javac Hello.java
📦 Hello.class
Bytecode (platform-independent)
⬇️ java Hello
🏭 JVM
╱       |       ╲
🔍 ClassLoader
loads .class
✅ Verifier
security check
⚡ JIT Compiler
→ machine code
⬇️
🖥️ Native Machine Code Execution
💡

JIT Compiler (Just-In-Time)

The JVM initially interprets bytecode line-by-line (slow). The JIT compiler identifies "hot" methods called frequently and compiles them to native machine code for maximum speed. This is why Java gets faster the longer it runs!

🧠 JVM Memory Model (Heap & Stack)

Understanding memory is crucial for debugging OutOfMemoryError, StackOverflowError, and for writing efficient code.

🧠 JVM Memory Areas
📚 STACK (per thread)
• Method calls (frames)
• Local variables
• Primitive values
• References to objects
• LIFO order
• Auto-cleaned on method exit
🏠 HEAP (shared)
• Objects (new keyword)
• Instance variables
• Arrays
• String Pool
• Managed by GC
• Young Gen + Old Gen
Method Area
Class data, static vars
PC Register
Current instruction
Native Stack
Native method calls
MemoryExample.java
public class MemoryExample { public static void main(String[] args) { int x = 10; // 🔵 STACK: primitive stored directly String name = "Java"; // 🔵 STACK: reference → 🟢 HEAP: String object int[] arr = new int[5]; // 🔵 STACK: reference → 🟢 HEAP: array object Car myCar = new Car(); // 🔵 STACK: reference → 🟢 HEAP: Car object } // When main() finishes: // Stack frame is popped (x, name ref, arr ref gone) // Heap objects become eligible for Garbage Collection }
⚠️

StackOverflowError vs OutOfMemoryError

StackOverflow: Too many method calls (usually infinite recursion). OutOfMemory: Heap is full — too many objects, memory leaks, or insufficient heap size. Fix with -Xmx flag.

🚀 Your First Java Program

HelloWorld.java
// Every Java program needs a class public class HelloWorld { // Class name = File name // Entry point — JVM calls this first public static void main(String[] args) { System.out.println("Hello, World! ☕"); } } // Breakdown of: public static void main(String[] args) // ┌─────────┬────────┬──────┬──────┬──────────────┐ // │ public │ static │ void │ main │ String[] args│ // ├─────────┼────────┼──────┼──────┼──────────────┤ // │ Access │ No obj │ No │ Name │ Command-line │ // │ to all │ needed │return│ JVM │ arguments │ // │ │to call │ val │ looks│ │ // │ │ │ │ for │ │ // └─────────┴────────┴──────┴──────┴──────────────┘
Chapter 02
Data Types & Variables
Java is strictly typed — every variable needs a declared type. Master the 8 primitives and reference types.
📊

📊 The 8 Primitive Data Types

📊 Primitive Types — Size, Range & Default Values
byte1 byte
short2 bytes
int4 bytes
long8 bytes
float4 bytes
double8 bytes
char2 bytes
boolean~1 bit
TypeSizeRangeDefaultExample
byte1 byte-128 to 1270byte b = 100;
short2 bytes-32,768 to 32,7670short s = 30000;
int4 bytes~±2.1 billion0int i = 42;
long8 bytes~±9.2 × 10¹⁸0Llong l = 123456789L;
float4 bytes~7 decimal digits0.0ffloat f = 3.14f;
double8 bytes~15 decimal digits0.0double d = 3.14159;
char2 bytes'\u0000' to '\uffff''\u0000'char c = 'A';
boolean~1 bittrue / falsefalseboolean b = true;
Primitives.java
// Integer Types byte age = 25; short year = 2025; int population = 1_000_000; // Underscores for readability! (Java 7+) long distance = 9_460_730_472_580L; // Must have 'L' suffix // Floating Point float pi = 3.14159f; // Must have 'f' suffix double precise = 3.141592653589793; // Character & Boolean char letter = 'A'; // Single quotes only! char unicode = '\u0041'; // Also 'A' (Unicode) boolean isActive = true; // Number Systems int binary = 0b1010; // Binary: 10 int octal = 012; // Octal: 10 int hex = 0xA; // Hex: 10

📦 Reference Types & Wrapper Classes

Primitives are stored directly on the stack. Reference types (objects) store a reference on the stack pointing to actual data on the heap. Java provides wrapper classes for each primitive.

🔄 Primitive ↔ Wrapper Mapping
byte → Byte
short → Short
int → Integer
long → Long
float → Float
double → Double
char → Character
boolean → Boolean
WrapperClasses.java
// Autoboxing: primitive → wrapper (automatic) Integer num = 42; // int → Integer (autoboxing) Double price = 9.99; // double → Double // Unboxing: wrapper → primitive (automatic) int n = num; // Integer → int (unboxing) // Why wrappers? Collections can't hold primitives! ArrayList<Integer> list = new ArrayList<>(); list.add(10); // autoboxes int → Integer // Useful wrapper methods Integer.parseInt("42"); // String → int: 42 String.valueOf(42); // int → String: "42" Integer.MAX_VALUE; // 2147483647 Integer.MIN_VALUE; // -2147483648 Double.isNaN(0.0/0.0); // true // ⚠️ Beware: == vs .equals() with wrappers Integer a = 128, b = 128; a == b; // false! (different objects beyond -128..127 cache) a.equals(b); // true ✅ (compare values) Integer c = 127, d = 127; c == d; // true! (Integer cache: -128 to 127)
🔥

Integer Cache Gotcha

Java caches Integer values from -128 to 127. Within this range, == works because both variables point to the same cached object. Outside this range, always use .equals()!

🔄 Type Casting & Promotion

📈 Widening (Implicit) — No data loss, automatic
byteshortintlongfloatdouble
📉 Narrowing (Explicit) — Possible data loss, requires cast
doublefloatlongintshortbyte
TypeCasting.java
// Widening (implicit) — safe, automatic int x = 100; long y = x; // int → long ✅ (no cast needed) double z = x; // int → double ✅ // Narrowing (explicit) — must cast manually double pi = 3.14159; int approx = (int) pi; // 3 (decimal truncated, NOT rounded!) long big = 1000000; int small = (int) big; // Works if value fits // Type Promotion in expressions byte a = 10, b = 20; // byte c = a + b; // ❌ Error! a+b promotes to int int c = a + b; // ✅ Correct byte d = (byte)(a + b); // ✅ Explicit cast
Chapter 03
Operators & Control Flow
Every operator Java offers, plus branching and looping constructs to control program execution.
🔀

🧮 All Operator Types

CategoryOperatorsExample
Arithmetic+ - * / %10 % 3 = 1
Assignment= += -= *= /= %=x += 5 (x = x+5)
Comparison== != > < >= <=5 > 3 → true
Logical&& || !true && false → false
Bitwise& | ^ ~ << >> >>>5 & 3 → 1
Ternary? :x > 0 ? "pos" : "neg"
instanceofinstanceofobj instanceof String
Unary++ -- + - !i++ vs ++i
📌

Short-Circuit Evaluation

&& stops evaluating if first operand is false. || stops if first is true. This matters when the second operand has side effects (e.g., method calls).

Operators.java
// Pre vs Post increment int a = 5; int b = a++; // b = 5, a = 6 (use THEN increment) int c = ++a; // a = 7, c = 7 (increment THEN use) // Integer division truncates int result = 7 / 2; // 3 (not 3.5!) double r = 7.0 / 2; // 3.5 ✅ // Ternary operator int age = 20; String status = (age >= 18) ? "Adult" : "Minor"; // instanceof (Java 16+ pattern matching) Object obj = "Hello"; if (obj instanceof String s) { System.out.println(s.toUpperCase()); // No cast needed! }

🔀 Branching & Loops

ControlFlow.java
// ═══ IF-ELSE ═══ int score = 85; if (score >= 90) { System.out.println("A"); } else if (score >= 80) { System.out.println("B"); // ← runs } else if (score >= 70) { System.out.println("C"); } else { System.out.println("F"); } // ═══ ENHANCED SWITCH (Java 14+) ═══ String day = "WED"; String type = switch (day) { case "MON", "TUE", "WED", "THU", "FRI" -> "Weekday"; case "SAT", "SUN" -> "Weekend"; default -> "Unknown"; }; // ═══ FOR LOOP ═══ for (int i = 0; i < 5; i++) { System.out.print(i + " "); // 0 1 2 3 4 } // ═══ ENHANCED FOR (For-Each) ═══ int[] nums = {10, 20, 30}; for (int n : nums) { System.out.print(n + " "); // 10 20 30 } // ═══ WHILE ═══ int count = 3; while (count > 0) { System.out.println("Count: " + count--); } // ═══ DO-WHILE (runs at least once!) ═══ int x = 0; do { System.out.println("Runs once even if false!"); } while (x > 0); // ═══ LABELED BREAK ═══ outer: for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { if (i == 1 && j == 1) break outer; // Breaks BOTH loops } }
Chapter 04
Arrays & Strings
Fixed-size arrays, multi-dimensional arrays, String immutability, the String Pool, and StringBuilder.
📝

📦 Arrays — Fixed-Size Containers

📦 1D Array in Memory — int[] arr = {10, 20, 30, 40, 50}
10arr[0]
20arr[1]
30arr[2]
40arr[3]
50arr[4]
Contiguous memory | 0-indexed | Fixed length: arr.length = 5
Arrays.java
import java.util.Arrays; // Declaration + Initialization int[] nums = {10, 20, 30, 40, 50}; int[] empty = new int[5]; // [0, 0, 0, 0, 0] String[] names = new String[3]; // [null, null, null] // Access & Modify nums[0] = 99; // Set first element int last = nums[nums.length - 1]; // Get last element // Iterate for (int i = 0; i < nums.length; i++) { } // Classic for (int n : nums) { } // For-each // Useful java.util.Arrays methods Arrays.sort(nums); // Sort ascending Arrays.fill(empty, -1); // [-1,-1,-1,-1,-1] Arrays.toString(nums); // "[10, 20, 30, 40, 50]" Arrays.copyOf(nums, 3); // [10, 20, 30] Arrays.binarySearch(nums, 30); // index of 30 (sorted!) Arrays.equals(arr1, arr2); // Content comparison // 2D Array int[][] matrix = { {1, 2, 3}, {4, 5, 6}, {7, 8, 9} }; matrix[1][2]; // 6 (row 1, col 2) matrix.length; // 3 (rows) matrix[0].length; // 3 (cols in first row) // Jagged Array (uneven rows) int[][] jagged = new int[3][]; jagged[0] = new int[]{1, 2}; jagged[1] = new int[]{3, 4, 5}; jagged[2] = new int[]{6};

📜 Strings — Immutable & The String Pool

🏊 String Pool (Heap Memory)
🏊 String Pool (inside Heap)
"Hello"
"Java"
"World"
Literals reuse existing objects | new String() creates outside pool
StringMethods.java
// String Pool behavior String s1 = "Hello"; // Pool String s2 = "Hello"; // Same reference from pool String s3 = new String("Hello"); // NEW object on heap s1 == s2; // true ✅ (same pool reference) s1 == s3; // false ❌ (different objects) s1.equals(s3); // true ✅ (same VALUE) // ═══ Essential String Methods ═══ String s = "Hello, Java World!"; s.length(); // 18 s.charAt(0); // 'H' s.indexOf("Java"); // 7 s.lastIndexOf('o'); // 15 s.substring(7, 11); // "Java" s.toUpperCase(); // "HELLO, JAVA WORLD!" s.toLowerCase(); // "hello, java world!" s.trim(); // Remove leading/trailing whitespace s.strip(); // Java 11+ (handles Unicode spaces) s.replace("Java", "Python"); // "Hello, Python World!" s.contains("Java"); // true s.startsWith("Hello"); // true s.endsWith("!"); // true s.isEmpty(); // false s.isBlank(); // false (Java 11+) s.split(", "); // ["Hello", "Java World!"] s.toCharArray(); // char[] String.join("-", "a","b","c"); // "a-b-c" s.repeat(3); // Java 11+ (repeat 3 times) String.format("Hi %s, age %d", "Bob", 25); // Formatted string // ═══ Immutability — every operation creates NEW String ═══ String original = "Hello"; String modified = original.concat(" World"); // original is still "Hello" — unchanged!

🔨 StringBuilder vs StringBuffer

✅ StringBuilder

  • Mutable — modifies in-place
  • NOT thread-safe
  • Faster (no synchronization)
  • Use for single-threaded code

🔒 StringBuffer

  • Mutable — modifies in-place
  • Thread-safe (synchronized)
  • Slower (locking overhead)
  • Use for multi-threaded code
StringBuilderDemo.java
// ❌ BAD: String concatenation in loops (creates N objects) String result = ""; for (int i = 0; i < 10000; i++) { result += i; // Creates new String EACH iteration! O(n²) } // ✅ GOOD: StringBuilder (mutable, efficient) StringBuilder sb = new StringBuilder(); for (int i = 0; i < 10000; i++) { sb.append(i); // Modifies same object! O(n) } String result2 = sb.toString(); // StringBuilder Methods StringBuilder sb2 = new StringBuilder("Hello"); sb2.append(" World"); // "Hello World" sb2.insert(5, ","); // "Hello, World" sb2.delete(5, 6); // "Hello World" sb2.replace(0, 5, "Hi"); // "Hi World" sb2.reverse(); // "dlroW iH" sb2.length(); // 8
Chapter 05
OOP — Part 1
Classes, objects, constructors, the this keyword, static members, encapsulation, and access modifiers.
🏗️

🧱 Classes, Objects & Constructors

🧱 Anatomy of a Java Class
📐 class BankAccount
Fields (State): accountNumber, balance, ownerName
Constructor: BankAccount(String owner, double initial)
Methods (Behavior): deposit(), withdraw(), getBalance()
BankAccount.java
public class BankAccount { // ═══ FIELDS (Instance Variables) ═══ private String owner; private double balance; private static int totalAccounts = 0; // Shared across ALL instances // ═══ CONSTRUCTOR (No-arg) ═══ public BankAccount() { this("Unknown", 0.0); // Constructor chaining! } // ═══ CONSTRUCTOR (Parameterized) ═══ public BankAccount(String owner, double balance) { this.owner = owner; // 'this' differentiates field from param this.balance = balance; totalAccounts++; // Static — shared counter } // ═══ METHODS ═══ public void deposit(double amount) { if (amount > 0) { this.balance += amount; System.out.println("Deposited: $" + amount); } } public void withdraw(double amount) { if (amount > 0 && amount <= balance) { balance -= amount; } else { System.out.println("Insufficient funds!"); } } // Getters & Setters (Encapsulation) public double getBalance() { return balance; } public String getOwner() { return owner; } // Static method — belongs to CLASS, not object public static int getTotalAccounts() { return totalAccounts; } // toString override @Override public String toString() { return "Account{owner='" + owner + "', balance=$" + balance + "}"; } } // ═══ USAGE ═══ BankAccount acc1 = new BankAccount("Alice", 1000); BankAccount acc2 = new BankAccount("Bob", 500); acc1.deposit(250); acc1.withdraw(100); System.out.println(acc1); // Account{owner='Alice', balance=$1150.0} BankAccount.getTotalAccounts(); // 2

🔒 Access Modifiers

ModifierClassPackageSubclassWorldUse For
publicAPI methods, constants
protectedMethods for subclasses
default (none)Package-internal code
privateFields, helper methods
🔐

Rule of Thumb

Make fields private, provide public getters/setters. This is encapsulation — you control how your data is accessed and modified. Never expose fields directly!

⚡ static vs instance

📌 static (Class-level)

  • Belongs to the class, not any object
  • Shared among ALL instances
  • Accessed via ClassName.method()
  • Cannot use this keyword
  • Loaded when class loads
  • Example: Math.sqrt(), counters

🔹 instance (Object-level)

  • Belongs to a specific object
  • Each object has its own copy
  • Accessed via objectRef.method()
  • Can use this keyword
  • Created when new is called
  • Example: myList.size()
Chapter 06
OOP — Part 2
Inheritance, polymorphism, abstract classes, interfaces — the powerful pillars of object-oriented design.
🧬

🧬 Inheritance (extends)

🧬 Inheritance — "IS-A" Relationship
🔷 Vehicle (Parent)
brand, speed, accelerate(), brake()
╱            ╲
🚗 Car extends Vehicle
+ numDoors, + openTrunk()
🏍️ Motorcycle extends Vehicle
+ hasSidecar, + wheelie()
Inheritance.java
class Vehicle { protected String brand; protected int speed; public Vehicle(String brand) { this.brand = brand; this.speed = 0; } public void accelerate(int amount) { speed += amount; System.out.println(brand + " speed: " + speed + " km/h"); } public void brake() { speed = Math.max(0, speed - 20); } } class Car extends Vehicle { private int doors; public Car(String brand, int doors) { super(brand); // MUST call parent constructor first! this.doors = doors; } // Car-specific method public void honk() { System.out.println(brand + ": Beep beep! 🚗"); } @Override // POLYMORPHISM — override parent behavior public void accelerate(int amount) { super.accelerate(amount); // Call parent version if (speed > 200) System.out.println("⚠️ Speed limit!"); } } // Usage Car tesla = new Car("Tesla", 4); tesla.accelerate(100); // Inherited + overridden tesla.honk(); // Car-specific tesla.brake(); // Inherited from Vehicle // Polymorphism — Parent reference, Child object Vehicle v = new Car("BMW", 4); v.accelerate(50); // Calls Car's version (dynamic dispatch) // v.honk(); // ❌ Compile error! Vehicle doesn't know about honk()

🎭 Abstract Classes & Interfaces

AbstractAndInterface.java
// ═══ ABSTRACT CLASS ═══ // Can't be instantiated — meant to be extended abstract class Shape { String color; public Shape(String color) { this.color = color; } // Abstract: NO body — subclass MUST implement abstract double area(); abstract double perimeter(); // Concrete: has a body — inherited as-is public void describe() { System.out.printf("%s shape, area=%.2f%n", color, area()); } } // ═══ INTERFACE ═══ // Pure contract — defines WHAT, not HOW interface Drawable { void draw(); // abstract (implicit) default void resize(double factor) { // Java 8+ default method System.out.println("Resizing by " + factor); } static void info() { // Java 8+ static method System.out.println("Drawable interface"); } } interface Printable { void print(); } // ═══ IMPLEMENTATION ═══ // A class can extend ONE class + implement MULTIPLE interfaces class Circle extends Shape implements Drawable, Printable { double radius; public Circle(String color, double radius) { super(color); this.radius = radius; } @Override public double area() { return Math.PI * radius * radius; } @Override public double perimeter() { return 2 * Math.PI * radius; } @Override public void draw() { System.out.println("Drawing ⭕ r=" + radius); } @Override public void print() { System.out.println(toString()); } } Circle c = new Circle("Red", 5.0); c.describe(); // Red shape, area=78.54 c.draw(); // Drawing ⭕ r=5.0 c.resize(2); // Resizing by 2.0 (default method)
FeatureAbstract ClassInterface
Instantiate?❌ No❌ No
Constructor?✅ Yes❌ No
Fields?✅ Any typeOnly static final
Methods?Abstract + concreteAbstract + default + static
Inheritance?extends (single)implements (multiple)
Use when?"IS-A" with shared code"CAN-DO" capability
Chapter 07
Collections Framework
Java's built-in data structures — List, Set, Map, Queue — with performance comparisons and use cases.
📚

🗺️ Collections Hierarchy

🏛️ Java Collections Framework — Complete Map
Iterable<E>
⬇️
Collection<E>
╱        |        ╲
📋 List<E>
ArrayList, LinkedList, Vector
🎯 Set<E>
HashSet, TreeSet, LinkedHashSet
🚶 Queue<E>
PriorityQueue, ArrayDeque
🗺️ Map<K,V> ← separate hierarchy!
HashMap, TreeMap, LinkedHashMap, Hashtable

📋 List Implementations Compared

ListExamples.java
import java.util.*; // ═══ ArrayList (Dynamic array — most common) ═══ List<String> fruits = new ArrayList<>(); fruits.add("Apple"); // [Apple] fruits.add("Banana"); // [Apple, Banana] fruits.add(1, "Cherry"); // [Apple, Cherry, Banana] fruits.set(0, "Avocado"); // [Avocado, Cherry, Banana] fruits.get(0); // "Avocado" — O(1) ✅ fruits.remove("Banana"); // [Avocado, Cherry] fruits.contains("Cherry"); // true fruits.size(); // 2 fruits.isEmpty(); // false fruits.indexOf("Cherry"); // 1 // Immutable list (Java 9+) List<String> fixed = List.of("A", "B", "C"); // Cannot modify! // Sort Collections.sort(fruits); // Natural order fruits.sort(Comparator.reverseOrder()); // Reverse fruits.sort(Comparator.comparingInt(String::length)); // By length // Convert String[] arr = fruits.toArray(new String[0]); // List → Array List<String> list = Arrays.asList("X","Y","Z"); // Array → List (fixed size!) List<String> mutable = new ArrayList<>(Arrays.asList("X","Y")); // Truly mutable
OperationArrayListLinkedListVector
get(index)O(1)O(n)O(1)
add(end)O(1)*O(1)O(1)*
add(index)O(n)O(1)**O(n)
removeO(n)O(1)**O(n)
searchO(n)O(n)O(n)
Thread-safe?✅ (slow)
*Amortized O(1) — occasional resize. **O(1) if you already have the node reference.

🎯 Set & 🗺️ Map

SetAndMap.java
// ═══ HashSet — Unique elements, O(1) lookup ═══ Set<String> colors = new HashSet<>(); colors.add("Red"); colors.add("Blue"); colors.add("Red"); // Ignored! Already exists colors.contains("Blue"); // true — O(1) colors.remove("Red"); // true colors.size(); // 1 // TreeSet — Sorted! O(log n) TreeSet<Integer> sorted = new TreeSet<>(Arrays.asList(5,1,8,3)); // [1, 3, 5, 8] — automatically sorted sorted.first(); // 1 sorted.last(); // 8 sorted.ceiling(4); // 5 (smallest >= 4) sorted.floor(4); // 3 (largest <= 4) // ═══ HashMap — Key-Value pairs, O(1) operations ═══ Map<String, Integer> scores = new HashMap<>(); scores.put("Alice", 95); scores.put("Bob", 87); scores.put("Charlie", 92); scores.get("Alice"); // 95 scores.getOrDefault("Dave", 0); // 0 (safe) scores.containsKey("Bob"); // true scores.remove("Bob"); // removes Bob scores.putIfAbsent("Eve", 88); // Only if key missing scores.replace("Alice", 97); // Update existing // Iterate a Map for (Map.Entry<String, Integer> e : scores.entrySet()) { System.out.println(e.getKey() + ": " + e.getValue()); } // Frequency Counter Pattern (very common!) String word = "programming"; Map<Character, Integer> freq = new HashMap<>(); for (char c : word.toCharArray()) { freq.merge(c, 1, Integer::sum); // Java 8+ elegant way } // {p=1, r=2, o=1, g=2, a=1, m=2, i=1, n=1} // Map.of (Java 9+ immutable) Map<String, Integer> fixed = Map.of("A", 1, "B", 2);
Map TypeOrderNull Keys?Thread-safe?Get/Put
HashMapNo order1 null keyO(1)
LinkedHashMapInsertion order1 null keyO(1)
TreeMapSorted by key❌ No nullO(log n)
HashtableNo order❌ No null✅ (slow)O(1)
O(1)
ConcurrentHashMapNo order❌ No null✅ (fast)O(1)

🚶 Queue, Deque & PriorityQueue

🚶 Queue (FIFO) vs Stack (LIFO) vs PriorityQueue
🚶 Queue (FIFO)
First In First Out
IN → [A][B][C] → OUT
add/poll
📚 Stack (LIFO)
Last In First Out
TOP → [C]
         [B]
         [A]
push/pop
⭐ PriorityQueue
Smallest first (min-heap)
[1][3][5][8]
poll() → 1
QueueExamples.java
import java.util.*; // ═══ Queue (FIFO) using LinkedList ═══ Queue<String> queue = new LinkedList<>(); queue.offer("Alice"); // add to rear (safer than add()) queue.offer("Bob"); queue.offer("Charlie"); queue.peek(); // "Alice" (view front, no remove) queue.poll(); // "Alice" (remove front) queue.size(); // 2 // ═══ Deque (Double-Ended Queue) ═══ Deque<Integer> deque = new ArrayDeque<>(); deque.offerFirst(1); // Add to front deque.offerLast(2); // Add to rear deque.peekFirst(); // View front deque.peekLast(); // View rear deque.pollFirst(); // Remove from front deque.pollLast(); // Remove from rear // ═══ Use Deque as a STACK (preferred over Stack class) ═══ Deque<String> stack = new ArrayDeque<>(); stack.push("A"); // [A] stack.push("B"); // [B, A] stack.push("C"); // [C, B, A] stack.peek(); // "C" (top) stack.pop(); // "C" (remove top) // ═══ PriorityQueue (Min-Heap by default) ═══ PriorityQueue<Integer> pq = new PriorityQueue<>(); pq.offer(30); pq.offer(10); pq.offer(20); pq.poll(); // 10 (smallest first!) pq.poll(); // 20 pq.poll(); // 30 // Max-Heap PriorityQueue<Integer> maxPQ = new PriorityQueue<>(Comparator.reverseOrder()); maxPQ.offer(30); maxPQ.offer(10); maxPQ.offer(20); maxPQ.poll(); // 30 (largest first!)

⚖️ Comparable vs Comparator

ComparableVsComparator.java
// ═══ Comparable — Natural ordering (inside the class) ═══ class Student implements Comparable<Student> { String name; int grade; public Student(String name, int grade) { this.name = name; this.grade = grade; } @Override public int compareTo(Student other) { return Integer.compare(this.grade, other.grade); // By grade } } List<Student> students = new ArrayList<>(); students.add(new Student("Alice", 95)); students.add(new Student("Bob", 87)); Collections.sort(students); // Uses compareTo → sorted by grade // ═══ Comparator — External, multiple sort strategies ═══ Comparator<Student> byName = (a, b) -> a.name.compareTo(b.name); Comparator<Student> byGradeDesc = (a, b) -> b.grade - a.grade; Comparator<Student> byNameLen = Comparator.comparingInt(s -> s.name.length()); students.sort(byName); // Sort by name alphabetically students.sort(byGradeDesc); // Sort by grade descending // Chaining Comparators students.sort(Comparator .comparingInt((Student s) -> s.grade) .thenComparing(s -> s.name)); // By grade, then by name

🔹 Comparable

  • Interface: Comparable<T>
  • Method: compareTo(T o)
  • Defined inside the class
  • Single natural ordering
  • Collections.sort(list)

🔷 Comparator

  • Interface: Comparator<T>
  • Method: compare(T a, T b)
  • Defined outside the class
  • Multiple sort strategies
  • list.sort(comparator)

🛠️ Collections Utility Class

CollectionsUtility.java
import java.util.*; List<Integer> nums = new ArrayList<>(Arrays.asList(5,2,8,1,9,3)); Collections.sort(nums); // [1, 2, 3, 5, 8, 9] Collections.reverse(nums); // [9, 8, 5, 3, 2, 1] Collections.shuffle(nums); // Random order Collections.max(nums); // 9 Collections.min(nums); // 1 Collections.frequency(nums, 5); // How many 5s? Collections.swap(nums, 0, 1); // Swap indices Collections.fill(nums, 0); // All zeros Collections.nCopies(5, "x"); // ["x","x","x","x","x"] // Unmodifiable (read-only view) List<Integer> readOnly = Collections.unmodifiableList(nums); // Thread-safe wrapper List<Integer> syncList = Collections.synchronizedList(nums); // Singleton & Empty collections List<String> single = Collections.singletonList("only"); List<String> empty = Collections.emptyList();
Chapter 08
Exception Handling
Gracefully handle errors — from checked exceptions to custom error classes and try-with-resources.
🛡️

🌳 Exception Hierarchy

🌳 Java Exception Hierarchy
java.lang.Object
⬇️
🔴 Throwable
╱            ╲
❌ Error
(Don't catch!)
OutOfMemoryError
StackOverflowError
VirtualMachineError
⚠️ Exception
(Catch these!)
Checked ☑️
IOException
SQLException
FileNotFoundException
Unchecked ☐
NullPointerException
ArrayIndexOutOfBounds
ClassCastException
ArithmeticException

☑️ Checked Exceptions

  • Caught at compile time
  • Must handle with try-catch or declare with throws
  • IOException, SQLException, FileNotFoundException
  • Extend Exception (not RuntimeException)

☐ Unchecked Exceptions

  • Occur at runtime
  • Not required to handle (but you should!)
  • NullPointerException, IndexOutOfBounds
  • Extend RuntimeException

🛡️ try-catch-finally

ExceptionHandling.java
// ═══ Basic try-catch ═══ try { int result = 10 / 0; // ArithmeticException! } catch (ArithmeticException e) { System.out.println("Error: " + e.getMessage()); // "/ by zero" } // ═══ Multiple catch blocks (specific → general) ═══ try { String s = null; s.length(); // NullPointerException! } catch (NullPointerException e) { System.out.println("Null reference!"); } catch (RuntimeException e) { System.out.println("Runtime error"); } catch (Exception e) { System.out.println("General error"); } finally { System.out.println("Always runs! (cleanup)"); } // ═══ Multi-catch (Java 7+) ═══ try { // risky code } catch (IOException | SQLException e) { System.out.println("IO or SQL error: " + e.getMessage()); } // ═══ finally always executes ═══ // Even if there's a return in try block! public static int test() { try { return 1; } finally { System.out.println("Finally runs before return!"); } }

🔄 throw vs throws & Custom Exceptions

ThrowVsThrows.java
// ═══ throw — manually throw an exception ═══ public void setAge(int age) { if (age < 0) { throw new IllegalArgumentException("Age can't be negative!"); } this.age = age; } // ═══ throws — declare that method MAY throw ═══ public void readFile(String path) throws IOException { FileReader fr = new FileReader(path); // May fail // Caller must handle or re-declare throws } // ═══ Custom Exception ═══ class InsufficientFundsException extends Exception { private double amount; public InsufficientFundsException(double amount) { super("Insufficient funds! Short by $" + amount); this.amount = amount; } public double getAmount() { return amount; } } // Using custom exception public void withdraw(double amount) throws InsufficientFundsException { if (amount > balance) { throw new InsufficientFundsException(amount - balance); } balance -= amount; } // Calling code try { account.withdraw(1000); } catch (InsufficientFundsException e) { System.out.println(e.getMessage()); System.out.println("Short by: $" + e.getAmount()); }
📌

throw vs throws — Quick Difference

throw is used inside a method to actually throw an exception object. throws is used in the method signature to declare that the method might throw a checked exception.

🔒 Try-With-Resources (Java 7+)

Automatically closes resources (streams, connections, readers) when the try block exits — no need for manual finally cleanup.

TryWithResources.java
// ❌ OLD WAY — manual cleanup in finally FileReader fr = null; try { fr = new FileReader("data.txt"); // read file... } catch (IOException e) { e.printStackTrace(); } finally { if (fr != null) { try { fr.close(); } catch (IOException e) { } } } // ✅ NEW WAY — try-with-resources (auto-close!) try (FileReader fr2 = new FileReader("data.txt"); BufferedReader br = new BufferedReader(fr2)) { String line; while ((line = br.readLine()) != null) { System.out.println(line); } } catch (IOException e) { System.out.println("Error reading file: " + e.getMessage()); } // fr2 and br are automatically closed here! ✅ // Any class implementing AutoCloseable works! class MyResource implements AutoCloseable { @Override public void close() { System.out.println("Resource cleaned up!"); } }
Chapter 09
Multithreading
Run tasks in parallel — thread lifecycle, synchronization, locks, and the modern ExecutorService.

🔄 Thread Lifecycle

🔄 Thread States
NEW
Thread created
⬇️ start()
RUNNABLE
Ready or running
⬇️ sleep()/wait()
WAITING / TIMED_WAITING
Paused
⬆️ notify()/timeout
⬇️ synchronized
BLOCKED
Waiting for lock
⬆️ lock acquired
⬇️ run() completes
TERMINATED
Dead — cannot restart

🧵 Creating Threads (3 Ways)

CreatingThreads.java
// ═══ Method 1: Extend Thread class ═══ class MyThread extends Thread { @Override public void run() { for (int i = 0; i < 5; i++) { System.out.println(getName() + ": " + i); try { Thread.sleep(100); } catch (InterruptedException e) {} } } } new MyThread().start(); // start(), NOT run()! // ═══ Method 2: Implement Runnable (Preferred ⭐) ═══ class MyTask implements Runnable { @Override public void run() { System.out.println("Task on: " + Thread.currentThread().getName()); } } new Thread(new MyTask()).start(); // ═══ Method 3: Lambda (Java 8+ — cleanest) ═══ Thread t = new Thread(() -> { System.out.println("Lambda thread! 🧵"); }); t.start(); // ═══ Useful Thread Methods ═══ t.setName("Worker-1"); // Name the thread t.setPriority(Thread.MAX_PRIORITY); // 1-10 (hint only) t.setDaemon(true); // Background thread t.join(); // Wait for t to finish t.isAlive(); // Is thread still running? Thread.sleep(1000); // Pause current thread 1 second Thread.yield(); // Suggest scheduler to switch

🔒 Synchronization

When multiple threads access shared data, race conditions occur. Synchronization ensures only one thread accesses critical sections at a time.

Synchronization.java
// ═══ Problem: Race Condition ═══ class Counter { private int count = 0; // ❌ NOT thread-safe — race condition! public void increment() { count++; } // ✅ Solution 1: Synchronized method public synchronized void safeIncrement() { count++; } // ✅ Solution 2: Synchronized block (finer control) public void blockIncrement() { synchronized (this) { count++; } } } // ═══ wait() / notify() — Inter-thread communication ═══ class SharedBuffer { private int data; private boolean hasData = false; public synchronized void produce(int value) throws InterruptedException { while (hasData) { wait(); // Release lock, wait for consumer } data = value; hasData = true; System.out.println("Produced: " + value); notifyAll(); // Wake up waiting consumers } public synchronized int consume() throws InterruptedException { while (!hasData) { wait(); // Release lock, wait for producer } hasData = false; System.out.println("Consumed: " + data); notifyAll(); return data; } }

🏊 ExecutorService & Thread Pools

Instead of manually creating threads, use ExecutorService to manage a pool of reusable threads — much more efficient and professional.

ExecutorServiceDemo.java
import java.util.concurrent.*; // ═══ Fixed Thread Pool ═══ ExecutorService executor = Executors.newFixedThreadPool(3); // Submit Runnable tasks (no return value) for (int i = 0; i < 10; i++) { final int taskId = i; executor.submit(() -> { System.out.println("Task " + taskId + " on " + Thread.currentThread().getName()); }); } // Submit Callable tasks (WITH return value) Future<Integer> future = executor.submit(() -> { Thread.sleep(1000); return 42; // Return a value! }); try { Integer result = future.get(); // Blocks until done System.out.println("Result: " + result); // 42 } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } // ALWAYS shutdown! executor.shutdown(); // Finish current tasks, accept no new ones // executor.shutdownNow(); // Force stop all tasks // ═══ Other Pool Types ═══ Executors.newSingleThreadExecutor(); // 1 thread Executors.newCachedThreadPool(); // Creates as needed, reuses Executors.newScheduledThreadPool(2); // Delayed/periodic tasks // ═══ CompletableFuture (Java 8+ — modern async) ═══ CompletableFuture.supplyAsync(() -> "Hello") .thenApply(s -> s + " World") .thenAccept(System.out::println); // "Hello World"
⚠️

Always Shutdown ExecutorService!

If you don't call shutdown(), your program will hang forever because the thread pool keeps running. Use try-finally or try-with-resources patterns.

Chapter 10
Streams, Lambda & Modern Java
Functional programming in Java — lambda expressions, Streams API, Optional, records, generics, and file I/O.
🌊

λ Lambda Expressions

Lambdas are anonymous functions — concise syntax for implementing functional interfaces (interfaces with exactly one abstract method).

Lambdas.java
// Syntax: (parameters) -> expression // or: (parameters) -> { statements; } // ═══ Before Lambda ═══ Comparator<String> oldWay = new Comparator<String>() { @Override public int compare(String a, String b) { return a.length() - b.length(); } }; // ═══ With Lambda ═══ Comparator<String> newWay = (a, b) -> a.length() - b.length(); // ═══ Method Reference (even shorter) ═══ Comparator<String> refWay = Comparator.comparingInt(String::length); // ═══ Lambda Examples ═══ Runnable r = () -> System.out.println("Hello!"); Consumer<String> print = s -> System.out.println(s); Predicate<Integer> isEven = n -> n % 2 == 0; Function<String, Integer> toLen = String::length; Supplier<Double> random = Math::random; BiFunction<Integer,Integer,Integer> add = (a, b) -> a + b;
Functional InterfaceMethodInputOutputExample
Predicate<T>test(T)Tbooleann -> n > 0
Function<T,R>apply(T)TRs -> s.length()
Consumer<T>accept(T)Tvoids -> print(s)
Supplier<T>get()noneT() -> new ArrayList()
UnaryOperator<T>apply(T)TTs -> s.toUpperCase()
BinaryOperator<T>apply(T,T)T, TT(a,b) -> a + b

🌊 Streams API

Streams provide a declarative pipeline for processing collections. They are lazy (intermediate ops don't run until a terminal op is called) and don't modify the source.

🌊 Stream Pipeline: Source → Intermediate → Terminal
📦 Source
.stream() / .of() / .generate()
⬇️ lazy (not executed yet)
🔄 Intermediate Ops
filter, map, sorted, distinct, limit, skip, flatMap, peek
⬇️ triggers execution
🏁 Terminal Op
collect, forEach, reduce, count, min, max, toList, findFirst, anyMatch
StreamsAPI.java
import java.util.*; import java.util.stream.*; List<Integer> nums = List.of(1,2,3,4,5,6,7,8,9,10); // ═══ filter + map + collect ═══ List<Integer> evenDoubled = nums.stream() .filter(n -> n % 2 == 0) // [2, 4, 6, 8, 10] .map(n -> n * 2) // [4, 8, 12, 16, 20] .collect(Collectors.toList()); // Collect to List // ═══ reduce (aggregate) ═══ int sum = nums.stream().reduce(0, Integer::sum); // 55 int product = nums.stream().reduce(1, (a,b) -> a * b); // ═══ Common operations ═══ nums.stream().count(); // 10 nums.stream().max(Integer::compareTo).get(); // 10 nums.stream().min(Integer::compareTo).get(); // 1 nums.stream().distinct().toList(); // Remove dupes nums.stream().sorted().toList(); // Sorted nums.stream().limit(3).toList(); // [1, 2, 3] nums.stream().skip(7).toList(); // [8, 9, 10] nums.stream().findFirst().get(); // 1 nums.stream().anyMatch(n -> n > 5); // true nums.stream().allMatch(n -> n > 0); // true nums.stream().noneMatch(n -> n < 0); // true // ═══ Strings with Streams ═══ List<String> names = List.of("Alice", "Bob", "Charlie", "Alice"); String joined = names.stream() .map(String::toUpperCase) .distinct() .sorted() .collect(Collectors.joining(", ")); // "ALICE, BOB, CHARLIE" // ═══ Grouping & Partitioning ═══ Map<Integer, List<String>> byLength = names.stream() .collect(Collectors.groupingBy(String::length)); // {3=[Bob], 5=[Alice, Alice], 7=[Charlie]} Map<Boolean, List<Integer>> evenOdd = nums.stream() .collect(Collectors.partitioningBy(n -> n % 2 == 0)); // {true=[2,4,6,8,10], false=[1,3,5,7,9]} // ═══ flatMap (flatten nested structures) ═══ List<List<Integer>> nested = List.of(List.of(1,2), List.of(3,4), List.of(5)); List<Integer> flat = nested.stream() .flatMap(Collection::stream) .toList(); // [1, 2, 3, 4, 5]

🔒 Optional — No More NullPointerException

OptionalDemo.java
import java.util.Optional; // ═══ Creating Optional ═══ Optional<String> name = Optional.of("Alice"); // Must not be null Optional<String> maybe = Optional.ofNullable(null); // May be null Optional<String> empty = Optional.empty(); // Empty // ═══ Using Optional ═══ name.isPresent(); // true name.isEmpty(); // false (Java 11+) name.get(); // "Alice" (throws if empty!) // Safe access (preferred) name.orElse("Unknown"); // "Alice" (or default if empty) maybe.orElse("Unknown"); // "Unknown" maybe.orElseThrow(); // Throws NoSuchElementException maybe.orElseThrow(() -> new RuntimeException("Missing!")); // Chaining String upper = name .map(String::toUpperCase) // Optional("ALICE") .filter(s -> s.startsWith("A")) // Optional("ALICE") .orElse("N/A"); // "ALICE" // ifPresent name.ifPresent(n -> System.out.println("Hello, " + n)); name.ifPresentOrElse( n -> System.out.println("Found: " + n), () -> System.out.println("Not found") );

📦 Records & Sealed Classes (Java 17)

ModernJava.java
// ═══ Records (Java 16+) — Immutable data carriers ═══ // Auto-generates: constructor, getters, equals(), hashCode(), toString() record Point(int x, int y) {} Point p = new Point(3, 4); p.x(); // 3 (auto getter — no "get" prefix) p.y(); // 4 p.toString(); // "Point[x=3, y=4]" // Record with custom method record Person(String name, int age) { // Compact constructor (validation) public Person { if (age < 0) throw new IllegalArgumentException("No negative age"); } public String greeting() { return "Hi, I'm " + name + ", age " + age; } } // ═══ Sealed Classes (Java 17) — Restrict who can extend ═══ sealed interface Shape permits Circle, Rectangle, Triangle {} record Circle(double radius) implements Shape {} record Rectangle(double w, double h) implements Shape {} final class Triangle implements Shape { double base, height; } // No other class can implement Shape! ✅ // ═══ Pattern Matching for switch (Java 21) ═══ String describe(Shape shape) { return switch (shape) { case Circle c -> "Circle r=" + c.radius(); case Rectangle r -> "Rect " + r.w() + "x" + r.h(); case Triangle t -> "Triangle"; }; }

🔤 Generics & Wildcards

Generics.java
// ═══ Generic Class ═══ class Box<T> { private T item; public void put(T item) { this.item = item; } public T get() { return item; } } Box<String> strBox = new Box<>(); strBox.put("Hello"); String s = strBox.get(); // No casting needed! Type-safe ✅ // ═══ Generic Method ═══ public static <T> T firstOrDefault(List<T> list, T defaultVal) { return list.isEmpty() ? defaultVal : list.get(0); } // ═══ Bounded Type Parameters ═══ public static <T extends Comparable<T>> T max(T a, T b) { return a.compareTo(b) >= 0 ? a : b; } // ═══ Wildcards ═══ // ? extends T → Upper bound (read-only, "get" from) public void printAll(List<? extends Number> list) { for (Number n : list) { System.out.println(n); } // list.add(1); // ❌ Can't add! (read-only) } // ? super T → Lower bound (write-only, "put" into) public void addNumbers(List<? super Integer> list) { list.add(1); // ✅ Can add Integer list.add(2); } // PECS: Producer Extends, Consumer Super
🌟

PECS Principle (Joshua Bloch)

Producer Extends — use ? extends T when you only READ from a structure. Consumer Super — use ? super T when you only WRITE to a structure. This maximizes API flexibility.

📁 File I/O (NIO.2)

FileIO.java
import java.nio.file.*; import java.io.*; import java.util.*; // ═══ Reading Files (Modern NIO.2 — Java 7+) ═══ // Read entire file as String String content = Files.readString(Path.of("data.txt")); // Read all lines as List List<String> lines = Files.readAllLines(Path.of("data.txt")); // Read with Stream (lazy, memory-efficient for large files) try (Stream<String> stream = Files.lines(Path.of("data.txt"))) { stream.filter(line -> line.contains("Java")) .forEach(System.out::println); } // ═══ Writing Files ═══ Files.writeString(Path.of("output.txt"), "Hello Java!"); Files.write(Path.of("lines.txt"), List.of("Line1", "Line2", "Line3")); // Append mode Files.writeString(Path.of("log.txt"), "New log entry\n", StandardOpenOption.APPEND, StandardOpenOption.CREATE); // ═══ Path Operations ═══ Path path = Path.of("/home/user/docs/file.txt"); path.getFileName(); // file.txt path.getParent(); // /home/user/docs path.toAbsolutePath(); // Full absolute path Files.exists(path); // true/false Files.size(path); // File size in bytes // ═══ Directory Operations ═══ Files.createDirectory(Path.of("newDir")); Files.createDirectories(Path.of("a/b/c")); // Nested Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING); Files.move(source, target); Files.delete(path); // List files in directory try (Stream<Path> files = Files.list(Path.of("."))) { files.filter(Files::isRegularFile) .forEach(System.out::println); } // Walk directory tree recursively try (Stream<Path> walk = Files.walk(Path.of("src"))) { walk.filter(p -> p.toString().endsWith(".java")) .forEach(System.out::println); // All .java files }

🏆 Practice Problems — All Chapters

P01 Reverse a String without built-in reverse Easy
P02 Check if two strings are anagrams Easy
P03 Find duplicates in an array using HashMap Easy
P04 Implement a Stack using ArrayList Medium
P05 Build a Banking System with OOP (inheritance, interfaces) Medium
P06 Custom LinkedList with Iterator Medium
P07 Producer-Consumer problem with wait/notify Hard
P08 Word frequency counter using Streams Easy
P09 Generic Pair<A,B> class with equals/hashCode Medium
P10 File-based student records system (CRUD + sorting) Medium
P11 Thread-safe Singleton pattern (double-checked locking) Hard
P12 LRU Cache implementation using LinkedHashMap Medium

🎯 LeetCode Problems — Java Focus

#1 Two Sum Easy
#20 Valid Parentheses Easy
#21 Merge Two Sorted Lists Easy
#49 Group Anagrams Medium
#53 Maximum Subarray Medium
#146 LRU Cache Medium
#206 Reverse Linked List Easy
#208 Implement Trie Medium
#347 Top K Frequent Elements Medium
#380 Insert Delete GetRandom O(1) Medium
#460 LFU Cache Hard
#295 Find Median from Data Stream Hard

🎉 Congratulations!

You've completed the ultimate Java notes — from JVM architecture to modern Java 17+ features. Keep coding, keep building!

☕🚀💪

"First, solve the problem. Then, write the code." — John Johnson

Chapters: 10  |  Topics: 50+  |  Code Examples: 50+  |  Practice: 24 Problems  |  Level: Beginner → Advanced