Comprehensive Guide to Java CompletableFuture: Asynchronous Tasks, Callbacks, and Multi‑Future Composition
This article explains Java's CompletableFuture API introduced in JDK 8, covering how to create asynchronous tasks with supplyAsync and runAsync, use various callback methods such as thenApply, thenAccept, thenRun, whenComplete, handle, and combine multiple futures with thenCombine, allOf, anyOf, providing code examples and execution results.
CompletableFutureis a new feature in JDK 8 that implements the CompletionStage interface and extends Future, adding asynchronous callbacks, stream processing, and the ability to compose multiple futures for smoother concurrent programming in Java.
1. Creating Asynchronous Tasks
1.1 supplyAsync
supplyAsynccreates an asynchronous task that returns a value. It provides two overloads: one that uses the default thread pool ( ForkJoinPool.commonPool()) and another that accepts a custom Executor.
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)Test code:
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> {
System.out.println("do something....");
return "result";
});
System.out.println("Result -> " + cf.get());
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executorService = Executors.newSingleThreadExecutor();
CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> {
System.out.println("do something....");
return "result";
}, executorService);
System.out.println("Result -> " + cf.get());
}Test results are shown in the accompanying screenshots.
1.2 runAsync
runAsynccreates an asynchronous task without a return value. It also has two overloads: one using the default thread pool and another that accepts a custom executor.
public static CompletableFuture<Void> runAsync(Runnable runnable)
public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor)Test code:
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture<Void> cf = CompletableFuture.runAsync(() -> {
System.out.println("do something....");
});
System.out.println("Result -> " + cf.get());
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executorService = Executors.newSingleThreadExecutor();
CompletableFuture<Void> cf = CompletableFuture.runAsync(() -> {
System.out.println("do something....");
}, executorService);
System.out.println("Result -> " + cf.get());
}Test results are displayed in the article.
2. Asynchronous Callback Handling
2.1 thenApply and thenApplyAsync
thenApplyexecutes a function after the preceding task completes, receiving the previous result as input and returning a new value. thenApplyAsync performs the same operation but runs in a separate thread (default ForkJoinPool.commonPool()) and can accept a custom executor.
CompletableFuture<Integer> cf1 = CompletableFuture.supplyAsync(() -> 1);
CompletableFuture<Integer> cf2 = cf1.thenApplyAsync(result -> result + 2);
System.out.println("cf1 result -> " + cf1.get());
System.out.println("cf2 result -> " + cf2.get());The screenshots illustrate that thenApply runs in the same thread as its predecessor, while thenApplyAsync may use a different thread.
2.2 thenAccept and thenAcceptAsync
thenAcceptruns a consumer after the previous task finishes; it receives the result but returns no value. The async variant behaves similarly but can run on a separate thread.
CompletableFuture<Integer> cf1 = CompletableFuture.supplyAsync(() -> 1);
CompletableFuture<Void> cf2 = cf1.thenAccept(result -> System.out.println("Result: " + result));
System.out.println("cf1 result -> " + cf1.get());
System.out.println("cf2 result -> " + cf2.get());2.3 thenRun and thenRunAsync
thenRunexecutes a runnable after the previous future completes, without receiving any input or producing a result. The async version may run on a different thread.
CompletableFuture<Integer> cf1 = CompletableFuture.supplyAsync(() -> 1);
CompletableFuture<Void> cf2 = cf1.thenRun(() -> System.out.println("Task finished"));
System.out.println("cf1 result -> " + cf1.get());
System.out.println("cf2 result -> " + cf2.get());2.4 whenComplete and whenCompleteAsync
whenCompleteruns a callback after a future finishes, receiving both the result (or null) and any exception that occurred. The async variant may execute in another thread.
CompletableFuture<Integer> cf1 = CompletableFuture.supplyAsync(() -> {
int a = 1 / 0; // will throw ArithmeticException
return 1;
});
CompletableFuture<Integer> cf2 = cf1.whenComplete((result, e) -> {
System.out.println("Previous result: " + result);
System.out.println("Previous exception: " + e);
});
System.out.println("cf2 result -> " + cf2.get());2.5 handle and handleAsync
handleis similar to whenComplete but allows the callback to return a new value, effectively transforming the result or providing a fallback when an exception occurs.
CompletableFuture<Integer> cf1 = CompletableFuture.supplyAsync(() -> 1);
CompletableFuture<Integer> cf2 = cf1.handle((result, e) -> {
System.out.println("Previous result: " + result);
System.out.println("Previous exception: " + e);
return result + 2;
});
System.out.println("cf2 result -> " + cf2.get());3. Multi‑Task Composition
3.1 thenCombine, thenAcceptBoth, runAfterBoth
These methods combine two CompletableFuture instances. All three wait for both tasks to complete successfully before proceeding. thenCombine receives both results and returns a new value; thenAcceptBoth receives both results but returns nothing; runAfterBoth runs after both complete without any input.
CompletableFuture<Integer> cf1 = CompletableFuture.supplyAsync(() -> 1);
CompletableFuture<Integer> cf2 = CompletableFuture.supplyAsync(() -> 2);
CompletableFuture<Integer> cf3 = cf1.thenCombine(cf2, (a, b) -> a + b);
System.out.println("cf3 result -> " + cf3.get());3.2 applyToEither, acceptEither, runAfterEither
These methods also combine two futures, but they trigger as soon as **any** one of the futures completes. applyToEither returns a value, acceptEither consumes the result without returning, and runAfterEither runs a runnable with no input.
3.3 allOf and anyOf
CompletableFuture.allOfreturns a new future that completes only when **all** supplied futures finish; if any completes exceptionally, the resulting future throws an exception. anyOf completes when **any** of the supplied futures finishes, returning the result of the first completed task.
CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() -> {
Thread.sleep(2000);
return "cf1 finished";
});
CompletableFuture<String> cf2 = CompletableFuture.supplyAsync(() -> {
Thread.sleep(5000);
return "cf2 finished";
});
CompletableFuture<Void> all = CompletableFuture.allOf(cf1, cf2);
System.out.println("All result -> " + all.get());
CompletableFuture<Object> any = CompletableFuture.anyOf(cf1, cf2);
System.out.println("Any result -> " + any.get());The article concludes with a brief thank‑you note and source reference.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
