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(); // BlocksChaining
java
CompletableFuture.supplyAsync(() -> fetchUserId())
.thenApply(id -> fetchUserData(id)) // Transform
.thenApply(data -> processData(data)) // Transform
.thenAccept(result -> display(result)); // Consume4. 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
| Method | Purpose | Example |
|---|---|---|
| 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!