This is the level that separates good Java developers from great ones. Concurrency bugs are among the hardest to find and fix. Understanding the JVM's memory model, garbage collection, and classic design patterns lets you build systems that are correct, performant, and maintainable under real-world conditions.
Step 1 — Threads: The Foundation
public class ThreadBasics {
// Way 1: Extend Thread (less flexible — Java only allows single inheritance)
static class MyThread extends Thread {
@Override
public void run() {
System.out.println("Thread: " + getName() + " is running");
}
}
// Way 2: Implement Runnable (PREFERRED — separates task from thread)
static class MyTask implements Runnable {
private final String name;
MyTask(String name) { this.name = name; }
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println(name + " - iteration " + i +
" on thread: " + Thread.currentThread().getName());
try { Thread.sleep(100); } // pause 100ms, throws InterruptedException
catch (InterruptedException e) {
Thread.currentThread().interrupt(); // restore interrupt flag!
return;
}
}
}
}
public static void main(String[] args) throws InterruptedException {
// Starting threads
Thread t1 = new MyThread();
t1.setName("Worker-1");
t1.start(); // starts NEW thread — NEVER call run() directly!
Thread t2 = new Thread(new MyTask("Task-A"), "Worker-2");
t2.start();
// Lambda shorthand for Runnable
Thread t3 = new Thread(() -> System.out.println("Lambda thread!"), "Worker-3");
t3.start();
// join(): wait for a thread to finish before continuing
t1.join(); // main thread BLOCKS until t1 completes
t2.join();
t3.join();
System.out.println("All threads done");
// Thread states: NEW -> RUNNABLE -> BLOCKED/WAITING/TIMED_WAITING -> TERMINATED
System.out.println("t1 state: " + t1.getState()); // TERMINATED
// Daemon threads: background threads that die when main thread dies
Thread daemon = new Thread(() -> {
while (true) { /* background work */ }
});
daemon.setDaemon(true); // must set BEFORE start()
// daemon.start(); // dies automatically when JVM exits
}
}Step 2 — Thread Safety and Synchronization
When multiple threads access shared mutable state without proper synchronization, you get race conditions — bugs that only appear under concurrent load and are notoriously difficult to reproduce.
import java.util.concurrent.atomic.*;
import java.util.concurrent.locks.*;
public class ThreadSafety {
// ============================================================
// PROBLEM: Race Condition (unsynchronized counter)
// ============================================================
static int unsafeCount = 0;
static void unsafeIncrement() { unsafeCount++; } // NOT atomic: read, add, write = 3 ops
// If two threads both read 5, both compute 6, both write 6 → count becomes 6 not 7!
// ============================================================
// SOLUTION 1: synchronized keyword
// ============================================================
static int syncCount = 0;
static synchronized void syncIncrement() {
syncCount++; // only ONE thread can execute this at a time
}
// Also works on instance methods and blocks:
// synchronized(this) { ... } — locks on 'this' object
// synchronized(SomeClass.class) { ... } — locks on class object (for static data)
// ============================================================
// SOLUTION 2: AtomicInteger — lock-free, hardware-level CAS operations
// BEST for single variable counters/flags
// ============================================================
static AtomicInteger atomicCount = new AtomicInteger(0);
static void atomicIncrement() {
atomicCount.incrementAndGet(); // atomic! reads and increments in one CPU instruction
}
// Other useful Atomic classes: AtomicLong, AtomicBoolean, AtomicReference<T>
static AtomicReference<String> atomicRef = new AtomicReference<>("initial");
// ============================================================
// SOLUTION 3: ReentrantLock — explicit lock with more control
// Use when you need tryLock(), lockInterruptibly(), or multiple conditions
// ============================================================
static int lockCount = 0;
static final ReentrantLock lock = new ReentrantLock(true); // fair=true: FIFO ordering
static void lockIncrement() {
lock.lock(); // acquire lock — blocks if another thread holds it
try {
lockCount++;
} finally {
lock.unlock(); // ALWAYS unlock in finally — even if exception thrown!
}
}
// tryLock(): attempt to acquire without blocking
static boolean tryIncrement() {
if (lock.tryLock()) { // returns immediately: true=acquired, false=couldn't
try {
lockCount++;
return true;
} finally {
lock.unlock();
}
}
return false; // lock was held by another thread
}
// ============================================================
// SOLUTION 4: ReadWriteLock — multiple readers OR one writer
// Perfect for read-heavy scenarios (caches, configuration)
// ============================================================
static final ReadWriteLock rwLock = new ReentrantReadWriteLock();
static String config = "default";
static String readConfig() {
rwLock.readLock().lock(); // multiple threads can hold read lock simultaneously
try {
return config;
} finally {
rwLock.readLock().unlock();
}
}
static void writeConfig(String newVal) {
rwLock.writeLock().lock(); // exclusive: no other readers or writers
try {
config = newVal;
} finally {
rwLock.writeLock().unlock();
}
}
// ============================================================
// volatile: ensures visibility across threads (NOT atomicity!)
// ============================================================
// Without volatile, JVM may cache the value in a thread-local register
static volatile boolean running = true;
// safe to use volatile for simple flags read/written by one thread, read by others
}Step 3 — ExecutorService: Thread Pool Management
Creating a new Thread for each task is expensive (creates OS thread, allocates stack memory). ExecutorService manages a pool of reusable threads, dramatically improving performance.
import java.util.concurrent.*;
import java.util.*;
public class ExecutorServiceExample {
public static void main(String[] args) throws Exception {
// ============================================================
// Common thread pool types
// ============================================================
// Fixed pool: always has N threads. Best for CPU-bound tasks.
ExecutorService fixedPool = Executors.newFixedThreadPool(4);
// Cached pool: creates threads on demand, reuses idle threads, 60s timeout.
// Best for short-lived async tasks. Risk: can create too many threads under load.
ExecutorService cachedPool = Executors.newCachedThreadPool();
// Single thread: guarantees sequential execution.
ExecutorService singleThread = Executors.newSingleThreadExecutor();
// Scheduled pool: for delayed or periodic tasks.
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);
// Virtual threads (Java 21+): massive scale-out for I/O-bound tasks
// ExecutorService virtualPool = Executors.newVirtualThreadPerTaskExecutor();
// ============================================================
// submit() with Callable<T>: returns Future<T>
// Callable = Runnable but can return a result and throw checked exceptions
// ============================================================
ExecutorService pool = Executors.newFixedThreadPool(3);
Callable<Integer> heavyComputation = () -> {
Thread.sleep(500);
return 42;
};
Future<Integer> future = pool.submit(heavyComputation);
// Do other work while computation runs on another thread...
System.out.println("Doing other work...");
// get() BLOCKS until the result is ready
Integer result = future.get(2, TimeUnit.SECONDS); // timeout: don't block forever!
System.out.println("Result: " + result); // 42
// ============================================================
// invokeAll(): submit multiple tasks, wait for ALL to finish
// ============================================================
List<Callable<String>> tasks = List.of(
() -> { Thread.sleep(100); return "Task-1 done"; },
() -> { Thread.sleep(200); return "Task-2 done"; },
() -> { Thread.sleep(50); return "Task-3 done"; }
);
List<Future<String>> futures = pool.invokeAll(tasks);
for (Future<String> f : futures) {
System.out.println(f.get()); // all guaranteed to be done
}
// ============================================================
// invokeAny(): submit multiple tasks, return FIRST result, cancel rest
// ============================================================
String fastest = pool.invokeAny(tasks);
System.out.println("Fastest: " + fastest); // whichever finished first
// ============================================================
// Scheduled tasks
// ============================================================
// Run once after 1 second delay
scheduler.schedule(() -> System.out.println("Delayed task!"), 1, TimeUnit.SECONDS);
// Run every 2 seconds (fixed rate)
ScheduledFuture<?> periodic = scheduler.scheduleAtFixedRate(
() -> System.out.println("Heartbeat: " + System.currentTimeMillis()),
0, 2, TimeUnit.SECONDS
);
Thread.sleep(5100);
periodic.cancel(false); // stop the periodic task
// ============================================================
// ALWAYS shut down executor service (otherwise JVM won't exit!)
// ============================================================
pool.shutdown(); // stop accepting new tasks, finish existing ones
if (!pool.awaitTermination(5, TimeUnit.SECONDS)) {
pool.shutdownNow(); // force cancel remaining tasks
}
scheduler.shutdown();
}
}Step 4 — CompletableFuture: Async, Non-Blocking Pipelines
import java.util.concurrent.*;
import java.util.*;
public class CompletableFutureExample {
// Simulated async service calls
static CompletableFuture<String> fetchUser(int id) {
return CompletableFuture.supplyAsync(() -> {
simulateDelay(300);
return "User-" + id;
});
}
static CompletableFuture<String> fetchOrders(String user) {
return CompletableFuture.supplyAsync(() -> {
simulateDelay(200);
return "Orders for " + user + ": [Order-1, Order-2]";
});
}
static CompletableFuture<Double> fetchExchangeRate() {
return CompletableFuture.supplyAsync(() -> {
simulateDelay(150);
return 83.5; // USD to INR
});
}
static void simulateDelay(long ms) {
try { Thread.sleep(ms); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
public static void main(String[] args) throws Exception {
// ============================================================
// Basic chain: supplyAsync -> thenApply -> thenAccept
// ============================================================
CompletableFuture<Void> pipeline = CompletableFuture
.supplyAsync(() -> " Hello World ") // runs on ForkJoinPool.commonPool()
.thenApply(String::trim) // transform (like Stream.map)
.thenApply(String::toUpperCase)
.thenAccept(System.out::println); // consume final result
pipeline.get(); // HELLO WORLD
// ============================================================
// Sequential chain: thenCompose (flatMap equivalent)
// When next step ITSELF returns a CompletableFuture
// ============================================================
CompletableFuture<String> ordersChain = fetchUser(1)
.thenCompose(user -> fetchOrders(user)); // sequential: user first, then orders
System.out.println(ordersChain.get());
// Orders for User-1: [Order-1, Order-2]
// ============================================================
// Parallel: thenCombine — wait for TWO independent futures
// ============================================================
CompletableFuture<String> userFuture = fetchUser(1);
CompletableFuture<Double> rateFuture = fetchExchangeRate();
// Both run in PARALLEL. Combine when BOTH complete.
CompletableFuture<String> combined = userFuture.thenCombine(
rateFuture,
(user, rate) -> user + " (rate: " + rate + ")"
);
System.out.println(combined.get()); // User-1 (rate: 83.5)
// ============================================================
// allOf: wait for ALL CompletableFutures
// ============================================================
CompletableFuture<String> f1 = fetchUser(1);
CompletableFuture<String> f2 = fetchUser(2);
CompletableFuture<String> f3 = fetchUser(3);
long start = System.currentTimeMillis();
CompletableFuture<Void> all = CompletableFuture.allOf(f1, f2, f3);
all.get(); // waits until ALL three complete
System.out.println("All done in: " + (System.currentTimeMillis() - start) + "ms");
// ~300ms (parallel) vs ~900ms (sequential)
// Collect results after allOf
List<String> users = List.of(f1.join(), f2.join(), f3.join());
System.out.println(users);
// ============================================================
// anyOf: return whichever completes FIRST
// ============================================================
CompletableFuture<Object> fastest = CompletableFuture.anyOf(
fetchUser(10), fetchExchangeRate(), fetchUser(20)
);
System.out.println("Fastest result: " + fastest.get());
// ============================================================
// Error handling
// ============================================================
CompletableFuture<String> withError = CompletableFuture
.supplyAsync(() -> {
if (true) throw new RuntimeException("Service unavailable");
return "never";
})
.exceptionally(ex -> {
System.out.println("Error caught: " + ex.getMessage());
return "fallback value"; // provide default on failure
})
.handle((result, ex) -> { // handle: receives result OR exception
if (ex != null) return "Error: " + ex.getMessage();
return result.toUpperCase();
});
System.out.println(withError.get());
// ============================================================
// Use custom executor (don't exhaust ForkJoinPool for I/O tasks)
// ============================================================
ExecutorService ioPool = Executors.newFixedThreadPool(10);
CompletableFuture<String> withCustomPool = CompletableFuture
.supplyAsync(() -> "result from I/O", ioPool) // 2nd arg: executor
.thenApplyAsync(String::toUpperCase, ioPool);
System.out.println(withCustomPool.get());
ioPool.shutdown();
}
}Step 5 — JVM Internals: Memory Areas and Garbage Collection
Understanding JVM memory areas lets you diagnose OutOfMemoryErrors, tune performance, and understand why certain code is slow.
JVM Memory Areas
- Heap: Where ALL objects are allocated. Divided into Young Generation (Eden + Survivor spaces) and Old Generation (Tenured). Managed by Garbage Collector. Shared across all threads.
- Stack: One per thread. Stores local variables, method call frames, and references to heap objects. Fixed size — StackOverflowError if exceeded (infinite recursion).
- Metaspace (Java 8+): Stores class metadata, method bytecode, static fields. Grows dynamically (no PermGen OOM). Configurable with -XX:MaxMetaspaceSize.
- Code Cache: Stores JIT-compiled native code. The JIT compiler watches hot methods and compiles them to native for speed.
- PC Register: Program Counter per thread — tracks which bytecode instruction the thread is currently executing.
Garbage Collection Algorithms
- Serial GC (-XX:+UseSerialGC): Single-threaded GC. Stop-the-world pauses. Only for single-core or tiny apps.
- Parallel GC (-XX:+UseParallelGC): Multi-threaded throughput GC. Good for batch processing. Still has stop-the-world pauses.
- G1 GC (-XX:+UseG1GC): Default since Java 9. Divides heap into regions. Concurrent + parallel. Targets predictable pause times. Best for most applications.
- ZGC (-XX:+UseZGC): Ultra-low latency. Sub-millisecond pauses regardless of heap size. Java 15+ production ready. Best for latency-critical apps.
- Shenandoah (-XX:+UseShenandoahGC): Similar to ZGC. Concurrent evacuation. OpenJDK only.
# Common JVM flags for production
java \
-Xms512m \ # initial heap size (set equal to Xmx to avoid resizing)
-Xmx2g \ # maximum heap size
-XX:+UseG1GC \ # use G1 garbage collector
-XX:MaxGCPauseMillis=200 \ # target max GC pause time (G1 tries to honor this)
-XX:+PrintGCDetails \ # log GC events (for diagnosis)
-XX:+HeapDumpOnOutOfMemoryError \ # dump heap on OOM for analysis
-XX:HeapDumpPath=/tmp/heap.hprof \
-XX:+UseStringDeduplication \ # G1: deduplicate identical Strings on heap
-Xss512k \ # thread stack size (reduce if creating many threads)
-XX:MetaspaceSize=256m \ # initial metaspace size
-server \ # use server JIT (more aggressive optimizations)
MyApplicationStep 6 — Concurrent Collections
import java.util.concurrent.*;
public class ConcurrentCollections {
public static void main(String[] args) throws InterruptedException {
// ============================================================
// ConcurrentHashMap: thread-safe Map
// Segment locking (not full-table lock) -> high concurrency
// ============================================================
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("a", 1);
map.put("b", 2);
// Atomic compound operations
map.computeIfAbsent("c", k -> k.length()); // compute only if key absent
map.merge("a", 10, Integer::sum); // merge: a = 1 + 10 = 11
System.out.println(map); // {a=11, b=2, c=1}
// ============================================================
// CopyOnWriteArrayList: thread-safe List for read-heavy workloads
// Write operations create a fresh copy of the underlying array
// Reads are NEVER locked — perfect for event listener lists
// ============================================================
CopyOnWriteArrayList<String> cowList = new CopyOnWriteArrayList<>();
cowList.add("listener1");
cowList.add("listener2");
// Iterate safely even while another thread is adding/removing
for (String s : cowList) { System.out.println(s); }
// ============================================================
// BlockingQueue: thread-safe FIFO queue with blocking operations
// Classic Producer-Consumer pattern
// ============================================================
BlockingQueue<String> queue = new LinkedBlockingQueue<>(10); // capacity 10
// Producer thread
Thread producer = new Thread(() -> {
try {
for (int i = 0; i < 5; i++) {
queue.put("item-" + i); // BLOCKS if queue is full
System.out.println("Produced: item-" + i);
Thread.sleep(100);
}
queue.put("STOP"); // poison pill to signal consumer to stop
} catch (InterruptedException e) { Thread.currentThread().interrupt(); }
});
// Consumer thread
Thread consumer = new Thread(() -> {
try {
while (true) {
String item = queue.take(); // BLOCKS if queue is empty
if ("STOP".equals(item)) break;
System.out.println("Consumed: " + item);
}
} catch (InterruptedException e) { Thread.currentThread().interrupt(); }
});
producer.start();
consumer.start();
producer.join();
consumer.join();
// Other BlockingQueue implementations:
// ArrayBlockingQueue — bounded, array-backed, fair ordering option
// PriorityBlockingQueue — unbounded, sorted by priority
// SynchronousQueue — zero capacity: put() blocks until another thread calls take()
// DelayQueue — elements can only be taken after their delay expires
}
}Step 7 — Design Patterns in Java
Design patterns are proven solutions to recurring design problems. The Gang of Four (GoF) book categorizes them into Creational, Structural, and Behavioral. Here are the most important ones with idiomatic Java implementations.
// PATTERN: Singleton (Creational)
// Ensures only ONE instance of a class exists
// Best implementation: Enum Singleton (Josh Bloch's recommendation)
public enum DatabaseConnection {
INSTANCE; // JVM guarantees single instantiation, thread-safe, serialization-safe
private final String url = "jdbc:postgresql://localhost/mydb";
public void query(String sql) {
System.out.println("Querying [" + url + "]: " + sql);
}
}
// Alternative: Double-checked locking (if you need a class, not enum)
class ConfigManager {
// volatile prevents instruction reordering during initialization
private static volatile ConfigManager instance;
private final String env;
private ConfigManager() {
this.env = System.getenv().getOrDefault("APP_ENV", "development");
}
public static ConfigManager getInstance() {
if (instance == null) { // first check: avoid locking every time
synchronized (ConfigManager.class) {
if (instance == null) { // second check: inside synchronized block
instance = new ConfigManager(); // safe initialization
}
}
}
return instance;
}
public String getEnv() { return env; }
}
class SingletonDemo {
public static void main(String[] args) {
DatabaseConnection.INSTANCE.query("SELECT * FROM users");
DatabaseConnection.INSTANCE.query("SELECT * FROM orders");
// Same INSTANCE used both times
System.out.println(ConfigManager.getInstance().getEnv());
}
}// PATTERN: Builder (Creational)
// Constructs complex objects step by step. Avoids telescoping constructors.
public class HttpRequest {
// All fields final — immutable after build
private final String method;
private final String url;
private final java.util.Map<String, String> headers;
private final String body;
private final int timeoutMs;
private final boolean followRedirects;
// Private constructor — only Builder can call it
private HttpRequest(Builder builder) {
this.method = builder.method;
this.url = builder.url;
this.headers = java.util.Collections.unmodifiableMap(builder.headers);
this.body = builder.body;
this.timeoutMs = builder.timeoutMs;
this.followRedirects = builder.followRedirects;
}
// Getters...
public String getMethod() { return method; }
public String getUrl() { return url; }
@Override
public String toString() {
return method + " " + url + " (timeout: " + timeoutMs + "ms)";
}
// Static nested Builder class
public static class Builder {
// Required fields
private final String method;
private final String url;
// Optional fields with defaults
private java.util.Map<String, String> headers = new java.util.HashMap<>();
private String body = null;
private int timeoutMs = 5000;
private boolean followRedirects = true;
public Builder(String method, String url) {
if (method == null || url == null) throw new NullPointerException();
this.method = method;
this.url = url;
}
public Builder header(String key, String value) {
this.headers.put(key, value);
return this; // return this to enable method chaining
}
public Builder body(String body) {
this.body = body; return this;
}
public Builder timeout(int ms) {
if (ms <= 0) throw new IllegalArgumentException("Timeout must be positive");
this.timeoutMs = ms; return this;
}
public Builder followRedirects(boolean follow) {
this.followRedirects = follow; return this;
}
public HttpRequest build() {
return new HttpRequest(this);
}
}
public static void main(String[] args) {
HttpRequest request = new HttpRequest.Builder("POST", "https://api.example.com/users")
.header("Content-Type", "application/json")
.header("Authorization", "Bearer token123")
.body("{\"name\": \"Kuldeep\"}")
.timeout(10000)
.followRedirects(false)
.build();
System.out.println(request);
// POST https://api.example.com/users (timeout: 10000ms)
}
}// PATTERN: Observer (Behavioral)
// Defines a one-to-many dependency: when one object changes state,
// all dependents are notified automatically.
import java.util.*;
// Subject (Observable)
class EventBus {
private final Map<String, List<EventListener>> listeners = new HashMap<>();
public void subscribe(String eventType, EventListener listener) {
listeners.computeIfAbsent(eventType, k -> new ArrayList<>()).add(listener);
}
public void unsubscribe(String eventType, EventListener listener) {
listeners.getOrDefault(eventType, List.of()).remove(listener);
}
public void publish(String eventType, Object data) {
List<EventListener> subs = listeners.getOrDefault(eventType, List.of());
subs.forEach(listener -> listener.onEvent(eventType, data));
}
}
// Observer interface
@FunctionalInterface
interface EventListener {
void onEvent(String eventType, Object data);
}
// Concrete observers
class EmailService {
public void sendWelcomeEmail(String eventType, Object data) {
System.out.println("[Email] Sending welcome email to: " + data);
}
}
class AuditLogger {
public void log(String eventType, Object data) {
System.out.println("[Audit] Event: " + eventType + ", Data: " + data);
}
}
class ObserverDemo {
public static void main(String[] args) {
EventBus bus = new EventBus();
EmailService emailService = new EmailService();
AuditLogger auditLogger = new AuditLogger();
// Subscribe — lambda works because EventListener is @FunctionalInterface
bus.subscribe("USER_REGISTERED", emailService::sendWelcomeEmail);
bus.subscribe("USER_REGISTERED", auditLogger::log);
bus.subscribe("ORDER_PLACED", auditLogger::log);
bus.subscribe("ORDER_PLACED", (type, data) ->
System.out.println("[Analytics] Tracking order: " + data));
// Publish events
bus.publish("USER_REGISTERED", "kuldeep@example.com");
// [Email] Sending welcome email to: kuldeep@example.com
// [Audit] Event: USER_REGISTERED, Data: kuldeep@example.com
bus.publish("ORDER_PLACED", "ORDER-12345");
// [Audit] Event: ORDER_PLACED, Data: ORDER-12345
// [Analytics] Tracking order: ORDER-12345
}
}// PATTERN: Strategy (Behavioral)
// Defines a family of algorithms, encapsulates each one,
// and makes them interchangeable at runtime.
import java.util.*;
// Strategy interface
@FunctionalInterface
interface SortStrategy {
void sort(int[] arr);
}
// Context: uses a strategy
class Sorter {
private SortStrategy strategy;
public Sorter(SortStrategy strategy) {
this.strategy = strategy;
}
public void setStrategy(SortStrategy strategy) { // swap strategy at runtime!
this.strategy = strategy;
}
public void sort(int[] arr) {
strategy.sort(arr);
}
}
// Concrete strategies
class BubbleSort implements SortStrategy {
@Override
public void sort(int[] arr) {
System.out.println("Using Bubble Sort...");
// O(n²) — for demonstration
for (int i = 0; i < arr.length - 1; i++)
for (int j = 0; j < arr.length - i - 1; j++)
if (arr[j] > arr[j+1]) { int t = arr[j]; arr[j] = arr[j+1]; arr[j+1] = t; }
}
}
class QuickSort implements SortStrategy {
@Override
public void sort(int[] arr) {
System.out.println("Using Quick Sort...");
Arrays.sort(arr); // simplified: Java's Arrays.sort uses Dual-Pivot Quicksort
}
}
class StrategyDemo {
public static void main(String[] args) {
int[] data = {5, 3, 8, 1, 9, 2};
Sorter sorter = new Sorter(new BubbleSort());
sorter.sort(data);
System.out.println(Arrays.toString(data));
// Swap strategy based on data size at runtime
if (data.length > 1000) {
sorter.setStrategy(new QuickSort());
}
// Even cleaner with lambdas:
sorter.setStrategy(arr -> Arrays.sort(arr)); // strategy is just a lambda!
}
}Other Essential Design Patterns
- Factory Method: Define an interface for creating objects, but let subclasses decide which class to instantiate. Used in java.util.Calendar.getInstance().
- Abstract Factory: Create families of related objects. Example: javax.xml.parsers.DocumentBuilderFactory.
- Decorator: Add behavior to objects dynamically. Java I/O streams are the canonical example: new BufferedReader(new FileReader(file)).
- Proxy: Control access to an object. Java's dynamic proxies (java.lang.reflect.Proxy) power Spring AOP and Hibernate lazy loading.
- Command: Encapsulate a request as an object. Enables undo/redo, queuing, and logging of operations.
- Template Method: Define skeleton of algorithm in base class, let subclasses fill in the steps. AbstractList in Java Collections is a great example.