Before You Start: What You Need
- Java Development Kit (JDK) 11 or later – Download from Oracle or OpenJDK. Version 11+ includes the
CompletableFutureAPI we'll use. - A text editor or IDE – IntelliJ IDEA Community Edition or VS Code with Java extensions work great.
- Basic Java knowledge – You should know how to write a class, a method, and use
System.out.println(). If you've written aforloop, you're ready. - A terminal – To compile and run your Java files.
No prior experience with async programming needed. We'll build everything from the ground up.
Why Async Programming? The Coffee Shop Analogy
Imagine you walk into a coffee shop. You order a latte. The barista starts making it. If you stand at the counter staring at the barista until your latte is done, you can't do anything else — that's synchronous (blocking) code. Your program freezes while waiting.
But what if you order, sit down, and chat with a friend while the barista works? When the latte is ready, the barista calls your name. That's asynchronous programming. Your program can do other tasks while waiting for a slow operation (like a network request or file read) to finish.
In Java, we have several ways to write async code. The modern, clean way is using CompletableFuture with async methods. This tutorial will teach you that approach by building a Sports Report Generator that fetches game scores from a fake API, processes them, and prints a summary — all without blocking the main thread.
Quick Refresher: What's a Future in Java?
Before async/await, Java used Future objects. A Future represents a result that hasn't been computed yet. Think of it like a ticket you get at a deli counter. You hand over your order, get a number, and can do other things until your number is called. When you're ready, you check the ticket to see if your order is ready.
Here's a simple Future example:
import java.util.concurrent.*;
public class FutureExample {
public static void main(String[] args) throws Exception {
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(() -> {
Thread.sleep(2000); // Simulate slow work
return "Scores fetched!";
});
System.out.println("Doing other work while waiting...");
String result = future.get(); // Blocks until done
System.out.println(result);
executor.shutdown();
}
}This works, but future.get() blocks the thread. If you have multiple tasks, you end up waiting sequentially. That's like ordering a coffee, then standing at the counter until it's done, then ordering a sandwich, and standing again. Not efficient.
Enter CompletableFuture: The Async Power Tool
CompletableFuture is Java's answer to JavaScript's Promise. It lets you chain async operations without blocking. Think of it as a smart ticket that not only holds your future result but also lets you say "when this is done, do that next."
Let's start building our Sports Report Generator. The project will:
- Fetch player stats from a fake API (simulated with delays).
- Fetch game results.
- Combine them into a report.
- Print the report.
All without blocking the main thread.
Step 1: Simulate a Slow API with CompletableFuture
First, create a class that simulates fetching data from a sports API. We'll use CompletableFuture.supplyAsync() to run a task on a background thread.
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
public class SportsApi {
public static CompletableFuture<String> fetchPlayerStats(String playerName) {
return CompletableFuture.supplyAsync(() -> {
try {
// Simulate network delay
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return playerName + " scored 24 points, 8 rebounds, 6 assists";
});
}
public static CompletableFuture<String> fetchGameResult(String homeTeam, String awayTeam) {
return CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return homeTeam + " " + (int)(Math.random() * 30 + 90) + " - " + (int)(Math.random() * 30 + 80) + " " + awayTeam;
});
}
}Notice: supplyAsync runs the lambda on a common ForkJoinPool. The method returns immediately with a CompletableFuture. The actual work happens in the background.
Step 2: The Old Way – Chaining with thenApply
Before async/await syntax, you'd chain operations using .thenApply(), .thenCompose(), etc. Let's see how that looks:
public class OldWayReport {
public static void main(String[] args) throws Exception {
CompletableFuture<String> playerFuture = SportsApi.fetchPlayerStats("LeBron James");
CompletableFuture<String> gameFuture = SportsApi.fetchGameResult("Lakers", "Celtics");
// Combine both results when both are done
CompletableFuture<String> reportFuture = playerFuture.thenCombine(gameFuture, (stats, game) -> {
return "=== SPORTS REPORT ===\nPlayer: " + stats + "\nGame: " + game;
});
// Wait for the final report (blocking, but only once)
String report = reportFuture.get();
System.out.println(report);
}
}This works, but the .thenCombine() chain can get messy with multiple steps. Imagine adding error handling, timeouts, or more data sources. The code becomes a pyramid of callbacks.
Step 3: The Clean Way – Async/Await with thenApply and thenCompose
Java doesn't have async/await keywords like JavaScript, but we can achieve the same readability using thenApply() and thenCompose(). Think of thenApply as "transform the result when it arrives" and thenCompose as "when this finishes, start another async task."
Let's rewrite the report generator using these methods:
import java.util.concurrent.CompletableFuture;
public class AsyncReportGenerator {
public static void main(String[] args) {
// Start both fetches in parallel
CompletableFuture<String> playerStats = SportsApi.fetchPlayerStats("Stephen Curry");
CompletableFuture<String> gameResult = SportsApi.fetchGameResult("Warriors", "Nuggets");
// When both are done, combine them into a report
CompletableFuture<Void> reportFuture = playerStats
.thenCombine(gameResult, (stats, game) -> {
return "=== GAME DAY REPORT ===\n" +
"Star Player: " + stats + "\n" +
"Final Score: " + game;
})
.thenAccept(report -> System.out.println(report));
// Keep the main thread alive until the async work finishes
reportFuture.join();
}
}Run this. You'll see the report printed after about 3 seconds (the longer of the two delays). The main thread didn't block during those 3 seconds — it could have done other work if we had more code before join().
Step 4: Error Handling – The Safety Net
Real APIs fail. Network drops, server errors, timeouts. You need to handle failures gracefully. With CompletableFuture, you use exceptionally() or handle().
Let's modify our SportsApi to sometimes fail:
public static CompletableFuture<String> fetchPlayerStats(String playerName) {
return CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(2);
if (Math.random() < 0.3) { // 30% chance of failure
throw new RuntimeException("Network timeout fetching " + playerName);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return playerName + " scored 24 points, 8 rebounds, 6 assists";
});
}Now add error handling to the report generator:
public class SafeReportGenerator {
public static void main(String[] args) {
CompletableFuture<String> playerStats = SportsApi.fetchPlayerStats("Giannis Antetokounmpo");
CompletableFuture<String> gameResult = SportsApi.fetchGameResult("Bucks", "76ers");
CompletableFuture<Void> reportFuture = playerStats
.thenCombine(gameResult, (stats, game) -> {
return "=== GAME DAY REPORT ===\n" +
"Star Player: " + stats + "\n" +
"Final Score: " + game;
})
.exceptionally(ex -> {
System.err.println("Failed to generate report: " + ex.getMessage());
return "Error generating report";
})
.thenAccept(report -> System.out.println(report));
reportFuture.join();
}
}exceptionally() catches any exception from the upstream futures and provides a fallback value. This is like having a backup plan if the barista spills your coffee.
Step 5: Building the Full Report Generator
Now let's build a complete, reusable report generator. It will fetch multiple player stats and game results, then combine everything into a formatted report.
import java.util.concurrent.CompletableFuture;
import java.util.List;
import java.util.stream.Collectors;
public class ReportGenerator {
public static CompletableFuture<String> generateFullReport() {
// Fetch multiple data sources in parallel
CompletableFuture<String> player1 = SportsApi.fetchPlayerStats("Luka Doncic");
CompletableFuture<String> player2 = SportsApi.fetchPlayerStats("Jayson Tatum");
CompletableFuture<String> game1 = SportsApi.fetchGameResult("Mavericks", "Celtics");
CompletableFuture<String> game2 = SportsApi.fetchGameResult("Bucks", "Heat");
// Combine all into a single report
CompletableFuture<String> combined = CompletableFuture
.allOf(player1, player2, game1, game2)
.thenApply(v -> {
String p1 = player1.join();
String p2 = player2.join();
String g1 = game1.join();
String g2 = game2.join();
return "=== FULL SPORTS REPORT ===\n" +
"Player 1: " + p1 + "\n" +
"Player 2: " + p2 + "\n" +
"Game 1: " + g1 + "\n" +
"Game 2: " + g2 + "\n" +
"===========================";
})
.exceptionally(ex -> {
System.err.println("Report generation failed: " + ex.getMessage());
return "Report unavailable due to error.";
});
return combined;
}
public static void main(String[] args) {
CompletableFuture<String> report = generateFullReport();
// Do other work here if needed
System.out.println("Generating report...");
String result = report.join();
System.out.println(result);
}
}allOf() waits for all provided futures to complete. Then thenApply runs after all are done. This is like ordering multiple items at a coffee shop — you get a ticket for each, and when all are ready, you pick them up together.
Step 6: Timeouts – Don't Wait Forever
metimes an API takes too long. You don't want your report generator to hang indefinitely. Use orTimeout() to set a deadline:
public static CompletableFuture<String> fetchPlayerStatsWithTimeout(String playerName) {
return SportsApi.fetchPlayerStats(playerName)
.orTimeout(5, TimeUnit.SECONDS)
.exceptionally(ex -> playerName + " stats unavailable (timeout)");
}Now if the API takes longer than 5 seconds, the future completes exceptionally, and our fallback kicks in.
Step 6: Putting It All Together – The Final Report Generator
Let's build a complete, robust report generator that handles timeouts, errors, and combines multiple data sources elegantly.
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
public class FinalReportGenerator {
public static void main(String[] args) {
// Fetch data with timeouts
CompletableFuture<String> playerStats = SportsApi.fetchPlayerStats("Nikola Jokic")
.orTimeout(4, TimeUnit.SECONDS)
.exceptionally(ex -> "Jokic stats: unavailable (" + ex.getMessage() + ")");
CompletableFuture<String> gameResult = SportsApi.fetchGameResult("Nuggets", "Suns")
.orTimeout(4, TimeUnit.SECONDS)
.exceptionally(ex -> "Game result: unavailable (" + ex.getMessage() + ")");
// Combine and print
playerStats.thenCombine(gameResult, (stats, game) -> {
return "=== QUICK REPORT ===\n" +
"Player: " + stats + "\n" +
"Game: " + game;
})
.thenAccept(System.out::println)
.join(); // Keep main alive
}
}Run this multiple times. Sometimes you'll see "unavailable" messages (if the random failure triggers). The program never crashes — it gracefully handles errors.
Common Issues and How to Fix Them
join()blocks the main thread – Yes, but only once at the end. In a real application (like a web server), you wouldn't calljoin()in a request handler. You'd return theCompletableFutureto the framework.
- My program exits before async work finishes – The JVM doesn't wait for background threads. Always call
join()orget()on the final future, or useCountDownLatch.
InterruptedExceptionin sleep – Always restore the interrupt flag:Thread.currentThread().interrupt(). This is good practice.
- Too many threads –
supplyAsyncuses a shared thread pool. For heavy workloads, create a customExecutorServiceand pass it tosupplyAsync().
- Debugging async code – Use
System.out.printlnwith thread names:Thread.currentThread().getName(). This helps trace execution.
Next Steps
You've built a working async report generator. To go further:
- Replace the simulated delays with real HTTP calls using
java.net.http.HttpClient(available since Java 11). - Use
thenApplyAsync()to control which thread pool executes each step. - Explore
CompletableFuture.allOf()for fan-out/fan-in patterns. - Read about reactive streams with Project Reactor for more complex async flows.
Async programming in Java is a superpower. It makes your applications responsive and efficient. Start using CompletableFuture in your next project — your users will thank you for the snappy experience.
Memuat komentar...