Lesson Completion
Back to course

CompletableFuture: Asynchronous Programming

Advanced
25 minutes4.8Java

1. The Hook (The "Byte-Sized" Intro)

  • In a Nutshell: CompletableFuture (Java 8+) enables non-blocking asynchronous programming. Create with supplyAsync() (returns value) or runAsync() (void).
  • Chain operations: thenApply() (transform), thenAccept() (consume), thenCompose() (flatten).
  • Combine futures: thenCombine(), allOf(), anyOf().
  • Handle errors: exceptionally(), handle(). Unlike Future.get() (blocks), CompletableFuture never blocks main thread! Perfect for microservices, API calls, async workflows.

Think of restaurant. Old Future = order food, stand at counter blocking (blocking). CompletableFuture = order, get pager, continue shopping, pager buzzes when ready (non-blocking callback)!


2. Conceptual Clarity (The "Simple" Tier)

💡 The Analogy: The Food Delivery App

  • supplyAsync(): Place order (async)
  • thenApply(): "When ready, add extra sauce" (transform)
  • thenAccept(): "When ready, eat!" (consume)
  • Chaining: Order → Cook → Deliver → Eat (pipeline)

3. Technical Mastery (The "Deep Dive")

Basic Usage

java
// Create async task CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> { return "Result"; }); // Non-blocking callback future.thenAccept(result -> System.out.println(result)); // Or block if needed String result = future.get(); // Blocks

Chaining

java
CompletableFuture.supplyAsync(() -> fetchUserId()) .thenApply(id -> fetchUserData(id)) // Transform .thenApply(data -> processData(data)) // Transform .thenAccept(result -> display(result)); // Consume

4. Interactive & Applied Code

java
import java.util.concurrent.*; import java.util.*; public class CompletableFutureDemo { public static void main(String[] args) throws Exception { demonstrateBasics(); demonstrateChaining(); demonstrateCombining(); demonstrateErrorHandling(); } static void demonstrateBasics() throws Exception { System.out.println("=== BASICS ==="); // supplyAsync (returns value) CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> { sleep(1000); return "Hello"; }); // runAsync (void) CompletableFuture<Void> future2 = CompletableFuture.runAsync(() -> { sleep(1000); System.out.println("Task completed"); }); // Non-blocking System.out.println("Doing other work..."); // Get result (blocks) String result = future1.get(); System.out.println("Result: " + result); } static void demonstrateChaining() throws Exception { System.out.println("\n=== CHAINING ==="); CompletableFuture.supplyAsync(() -> { System.out.println("Fetching user ID..."); sleep(1000); return 123; }) .thenApply(id -> { System.out.println("Fetching user data for ID: " + id); sleep(1000); return "User-" + id; }) .thenApply(userData -> { System.out.println("Processing: " + userData); return userData.toUpperCase(); }) .thenAccept(result -> { System.out.println("Final result: " + result); }) .get(); // Wait for completion } static void demonstrateCombining() throws Exception { System.out.println("\n=== COMBINING ==="); CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> { sleep(1000); return "Hello"; }); CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> { sleep(1500); return "World"; }); // Combine two futures CompletableFuture<String> combined = future1.thenCombine(future2, (s1, s2) -> s1 + " " + s2); System.out.println("Combined: " + combined.get()); // Wait for all CompletableFuture<Void> all = CompletableFuture.allOf(future1, future2); all.get(); System.out.println("All completed"); // Wait for any CompletableFuture<Object> any = CompletableFuture.anyOf(future1, future2); System.out.println("First result: " + any.get()); } static void demonstrateErrorHandling() throws Exception { System.out.println("\n=== ERROR HANDLING ==="); // exceptionally (recover from error) CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> { if (Math.random() > 0.5) { throw new RuntimeException("Oops!"); } return "Success"; }).exceptionally(ex -> { System.out.println("Error: " + ex.getMessage()); return "Default value"; }); System.out.println("Result: " + future1.get()); // handle (handle both success and error) CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> { return "Result"; }).handle((result, ex) -> { if (ex != null) { return "Error occurred"; } return result.toUpperCase(); }); System.out.println("Handled: " + future2.get()); } static void sleep(int ms) { try { Thread.sleep(ms); } catch (Exception e) {} } } // Real-world example: Parallel API calls class MicroserviceClient { public static void main(String[] args) throws Exception { long start = System.currentTimeMillis(); // Sequential (slow) String user = fetchUser(); String orders = fetchOrders(); String inventory = fetchInventory(); long sequential = System.currentTimeMillis() - start; // Parallel (fast) start = System.currentTimeMillis(); CompletableFuture<String> userFuture = CompletableFuture.supplyAsync(() -> fetchUser()); CompletableFuture<String> ordersFuture = CompletableFuture.supplyAsync(() -> fetchOrders()); CompletableFuture<String> inventoryFuture = CompletableFuture.supplyAsync(() -> fetchInventory()); CompletableFuture.allOf(userFuture, ordersFuture, inventoryFuture).get(); long parallel = System.currentTimeMillis() - start; System.out.println("Sequential: " + sequential + "ms"); System.out.println("Parallel: " + parallel + "ms"); // Parallel is 3x faster! } static String fetchUser() { sleep(1000); return "User data"; } static String fetchOrders() { sleep(1000); return "Orders data"; } static String fetchInventory() { sleep(1000); return "Inventory data"; } static void sleep(int ms) { try { Thread.sleep(ms); } catch (Exception e) {} } }

5. The Comparison & Decision Layer

MethodPurposeExample
thenApply()Transform result.thenApply(s -> s.toUpperCase())
thenAccept()Consume result.thenAccept(s -> print(s))
thenRun()Run after completion.thenRun(() -> cleanup())
thenCompose()Flatten nested futures.thenCompose(id -> fetchUser(id))
thenCombine()Combine two futures.thenCombine(f2, (a,b) -> a+b)

6. The "Interview Corner" (The Edge)

The "Killer" Interview Question: "What's the difference between thenApply() and thenCompose()?" Answer: Flattening nested futures!

java
// thenApply() - returns CompletableFuture<CompletableFuture<T>> CompletableFuture<CompletableFuture<User>> nested = CompletableFuture.supplyAsync(() -> getUserId()) .thenApply(id -> fetchUser(id)); // Returns Future! // thenCompose() - flattens to CompletableFuture<T> CompletableFuture<User> flat = CompletableFuture.supplyAsync(() -> getUserId()) .thenCompose(id -> fetchUser(id)); // Flattens!

Think: thenApply() = map(), thenCompose() = flatMap() in streams!

Pro-Tip: Custom thread pool:

java
// Uses ForkJoinPool.commonPool() (default) CompletableFuture.supplyAsync(() -> task()); // Use custom thread pool Executor executor = Executors.newFixedThreadPool(10); CompletableFuture.supplyAsync(() -> task(), executor);

Always use custom pool for blocking I/O to avoid starving commonPool!

Topics Covered

Java FundamentalsConcurrency

Tags

#java#multithreading#threads#concurrency#parallelism

Last Updated

2025-02-01