In this article, we will explore CompletableFuture in Java 8 in-depth. Let’s get started.
What is CompletableFuture ?
CompletableFuture is introduced in Java 8. It facilitates asynchronous programming, concurrent operations, and asynchronous execution of tasks.
CompletableFuture implements the Future interface and CompletionStage interface.
CompletableFuture provides methods like runAsync() and supplyAsync() that runs a task asynchronously.
CompletableFuture also offers a vast selection of methods that let you attach callbacks that will be executed on completion.
Key Features of CompletableFuture
Here are the key features of CompletableFuture
in Java:
- Asynchronous Execution:
CompletableFuture
provides a way to perform asynchronous computations, allowing you to start and manage tasks that run concurrently. - Chaining Operations: You can chain multiple asynchronous operations together using methods like
thenApply
,thenAccept
, andthenCompose
, creating a sequence of actions to be performed when the previous one completes. - Non-Blocking Exception Handling:
CompletableFuture
offers methods likeexceptionally
,handle
, andwhenComplete
to handle exceptions that might occur during asynchronous computations. - Combining Results: You can combine the results of multiple
CompletableFuture
instances using methods likethenCombine
,thenCombineAsync
, andthenCompose
. - Asynchronous Execution Control: Methods like
thenRun
,thenRunAsync
, andthenRunAsync
allow you to specify tasks to run after the main computation completes, irrespective of the result. - Completing Futures: You can explicitly complete a
CompletableFuture
using methods likecomplete
,completeExceptionally
, andobtrudeValue
to set the result or exception. - Timeouts: The
orTimeout
method allows you to set a timeout for aCompletableFuture
, causing it to complete exceptionally if it takes too long to finish. - Support for Streams: Java 9 and later versions offer integration with the
CompletableFuture
API, allowing you to convert streams into asynchronous computations usingCompletableFuture
methods. - Cancellation: You can cancel a
CompletableFuture
using thecancel
method, which can propagate cancellation to dependent futures. - Parallelism and Concurrency:
CompletableFuture
enables better utilization of multicore processors by allowing tasks to be executed concurrently. - Flexible Result Handling:
CompletableFuture
provides various methods to retrieve results, including blocking methods likeget
and non-blocking methods likejoin
. - Executor Customization: You can choose different executors to control where and how tasks are executed, improving resource management and performance.
Important methods of CompletableFuture
Method | Description |
---|---|
thenApply | Applies a function to the result of the current CompletableFuture , producing a new CompletableFuture . |
thenAccept | Accepts a consumer function that operates on the result, producing a new CompletableFuture with Void result. |
thenRun | Executes a runnable action after the current CompletableFuture completes, producing a new CompletableFuture . |
thenCompose | Chains a new CompletableFuture by applying a function that returns a CompletableFuture to the current result. |
thenCombine | Combines two CompletableFuture results using a function, producing a new CompletableFuture with the result. |
thenEither | Returns a new CompletableFuture that completes when either of two given CompletableFuture completes. |
thenApplyAsync | Applies a function asynchronously to the result using the default executor. |
thenAcceptAsync | Performs an asynchronous action on the result using the default executor. |
thenRunAsync | Executes an asynchronous runnable action using the default executor. |
thenComposeAsync | Applies a function asynchronously using the default executor and returns a new CompletableFuture . |
thenCombineAsync | Combines two CompletableFuture asynchronously using the default executor. |
thenEitherAsync | Returns a new CompletableFuture that completes asynchronously when either of two given CompletableFuture completes. |
exceptionally | Handles exceptions and produces a new CompletableFuture with a replacement value or exception. |
handle | Handles exceptions or the result with a function, producing a new CompletableFuture . |
whenComplete | Executes an action on completion (either success or failure) and returns a new CompletableFuture . |
thenApplyToEither | Applies a function to the result of the first completed CompletableFuture among two. |
thenComposeToEither | Chains a new CompletableFuture by applying a function to the result of the first completed CompletableFuture . |
allOf | Combines an array of CompletableFuture into a single CompletableFuture that completes when all complete. |
anyOf | Returns a new CompletableFuture that completes when any of the given CompletableFuture completes. |
runAsync | Creates a new CompletableFuture and schedules a runnable action to execute asynchronously. |
supplyAsync | Creates a new CompletableFuture and schedules a supplier action to execute asynchronously. |
complete | Manually completes a CompletableFuture with a given value. |
cancel | Attempts to cancel the execution of the CompletableFuture . |
isDone | Checks if the CompletableFuture is completed, whether normally, exceptionally, or via cancellation. |
get | Retrieves the result value when it is available, waiting if necessary. |
Creating a Completed CompletableFuture
Creating a completed CompletableFuture
involves using the completedFuture
static factory method provided by the CompletableFuture
class. This method allows you to create a CompletableFuture
that is already completed with a given result.
CompletableFuture<String> predefined = CompletableFuture.completedFuture("predefined");
System.out.println("predefined result:"+predefined.get());
//Output
predefined
Running asynchronous computation using runAsync()
It takes a Runnable object and returns CompletableFuture<Void>.
It does not return anything. The get call returns null.
CompletableFuture<Void> runnable = CompletableFuture.runAsync(() -> {
System.out.println("executing runnable task with no result returned");
});
System.out.println("runnable result:"+runnable.get());
// Output
executing runnable task with no result returned
runnable result:null
Run a task asynchronously and return the result using supplyAsync()
It takes a Supplier<T> and returns CompletableFuture<T>
CompletableFuture<String> supplier = CompletableFuture.supplyAsync(() -> "test");
System.out.println("supplier result:"+supplier.get());
// Output
supplier result:test
Using ExecutorService for Thread Pool
CompletableFuture executes all tasks in threads obtained from the global ForkJoinPool.commonPool() by default unless we provide a Executor to it.
ExecutorService executor = Executors.newFixedThreadPool(10);
CompletableFuture<String> usingExecutor = CompletableFuture.supplyAsync(() -> {
return "Using ExecutorService Thread Pool";
}, executor);
System.out.println("usingExecutor result:"+usingExecutor.get());
// Output
usingExecutor result:Using ExecutorService Thread Pool
Using thenApply callback
You can use thenApply() method to process and transform the result of a CompletableFuture when it arrives.
It takes a Function<T,R> as an argument. Function<T,R> is a simple functional interface representing a function that accepts an argument of type T and produces a result of type R.
CompletableFuture<String> applyCallBack = CompletableFuture.supplyAsync(() -> "applyCallBack").thenApply(s -> {
return s.toUpperCase();
});
System.out.println("applyCallBack result:"+applyCallBack.get());
// Output
applyCallBack result:APPLYCALLBACK
Using thenAccept and theRun callback
If you don’t want to return anything from your callback function and just want to run some piece of code after the completion of the Future, then you can use thenAccept() and thenRun() methods. These methods are consumers and are often used as the last callback in the callback chain.
CompletableFuture.thenAccept() takes a Consumer<T> and returns CompletableFuture<Void>. It has access to the result of the CompletableFuture on which it is attached.
CompletableFuture.thenRun() doesn’t even have access to the Future’s result. It takes a Runnable and returns CompletableFuture<Void>
CompletableFuture<Void> acceptCallBack = CompletableFuture.supplyAsync(() -> "acceptCallBack").
thenAccept(x -> System.out.println(x + " example"));
System.out.println("acceptCallBack result:"+acceptCallBack.get());
CompletableFuture<Void> runCallBack = CompletableFuture.supplyAsync(() -> "runCallBack").
thenRun(()-> System.out.println("runCallBack"));
System.out.println("runCallBack result:"+runCallBack.get());
// Output
acceptCallBack example
acceptCallBack result:null
runCallBack
runCallBack result:null
Chaining multiple callbacks
You can also write a sequence of transformations on the CompletableFuture by attaching a series of thenApply() callback methods. The result of one thenApply() method is passed to the next in the series.
We can also attach thenAccept to the result of thenApply.
CompletableFuture<String> applySequenceCallBack = CompletableFuture.supplyAsync(() -> "applyCallBack").thenApply(s -> {
return s.toUpperCase();
}).thenApply(s -> {
return s + " Sequence".toUpperCase();
});
System.out.println("applySequenceCallBack result:"+applySequenceCallBack.get());
//Output
applySequenceCallBack result:APPLYCALLBACK SEQUENCE
Callback as a separate task using the async suffix
callbacks such thenApply, thenAccept and thenRun are executed on the same thread as their predecessor.
We can use thenApplySync, thenAcceptSync and thenRunSync for executing these as separate tasks in different threads.
CompletableFuture<String> thenApplyAsync = CompletableFuture.supplyAsync(() -> "thenApplyAsync").thenApplyAsync(s -> {
return s.toUpperCase();
});
System.out.println("thenApplyAsync result:"+thenApplyAsync.get());
// Output
thenApplyAsync result:THENAPPLYASYNC
Java Full Code
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CompletableFutureExample {
public static void main(String[] args) throws InterruptedException, ExecutionException {
CompletableFuture<String> predefined = CompletableFuture.completedFuture("predefined");
System.out.println("predefined result: " + predefined.get());
CompletableFuture<Void> runnable = CompletableFuture.runAsync(() -> {
System.out.println("executing runnable task with no result returned");
});
CompletableFuture<String> supplier = CompletableFuture.supplyAsync(() -> "test");
System.out.println("supplier result: " + supplier.get());
CompletableFuture<String> applyCallBack = supplier.thenApply(s -> {
return s.toUpperCase();
});
System.out.println("applyCallBack result: " + applyCallBack.get());
CompletableFuture<String> applySequenceCallBack = applyCallBack.thenApply(s -> {
return s + " Sequence".toUpperCase();
});
System.out.println("applySequenceCallBack result: " + applySequenceCallBack.get());
CompletableFuture<Void> acceptCallBack = supplier.thenAccept(x -> System.out.println(x + " example"));
System.out.println("acceptCallBack result: " + acceptCallBack.get());
CompletableFuture<Void> runCallBack = CompletableFuture.supplyAsync(() -> "runCallBack").thenRun(() -> {
System.out.println("runCallBack");
});
System.out.println("runCallBack result: " + runCallBack.get());
ExecutorService executor = Executors.newFixedThreadPool(10);
CompletableFuture<String> usingExecutor = CompletableFuture.supplyAsync(() -> {
return "Using ExecutorService Thread Pool";
}, executor);
System.out.println("usingExecutor result: " + usingExecutor.get());
CompletableFuture<String> thenApplyAsync = supplier.thenApplyAsync(s -> {
return s + " thenApplyAsync";
});
System.out.println("thenApplyAsync result: " + thenApplyAsync.get());
executor.shutdown();
}
}
Output
predefined result: predefined
executing runnable task with no result returned
supplier result: test
applyCallBack result: TEST
applySequenceCallBack result: TEST SEQUENCE
test example
acceptCallBack result: null
runCallBack
runCallBack result: null
usingExecutor result: Using ExecutorService Thread Pool
thenApplyAsync result: test thenApplyAsync
CompletableFuture Exception Handling
The CompletableFuture
class in Java provides several methods to handle exceptions that might occur during asynchronous computations. These methods allow you to gracefully manage exceptions and perform custom actions based on whether an exception occurs or not. Here are some key methods for handling exceptions in CompletableFuture
:
- exceptionally(Function<Throwable, T> handler): This method takes a function that handles exceptions if they occur during the computation. It returns a new
CompletableFuture
that represents the result after applying the exception handler. If an exception occurs, the provided function is invoked with the exception, and you can return a default value or alternative result. - handle(BiFunction<T, Throwable, U> handler): This method is more general-purpose and takes a function that handles both the result and any exception. It returns a new
CompletableFuture
that represents the result after applying the handler. If an exception occurs, the provided function is invoked with the exception, and you can return a default value or modified result. If no exception occurs, the function is invoked with the original result. - whenComplete(BiConsumer<T, Throwable> action): This method allows you to specify an action that is performed regardless of whether an exception occurs or not. It takes a
BiConsumer
that receives both the result and the exception. This method is useful when you want to perform some cleanup or logging after the computation completes. - handleAsync(BiFunction<T, Throwable, U> handler): This method is similar to
handle()
, but it executes the handler in a different thread (asynchronously) using the default executor. - exceptionallyAsync(Function<Throwable, T> handler): Similar to
exceptionally()
, this method executes the exception handler in a different thread (asynchronously) using the default executor. - whenCompleteAsync(BiConsumer<T, Throwable> action): Similar to
whenComplete()
, this method executes the action asynchronously using the default executor. - handleAsync(BiFunction<T, Throwable, U> handler, Executor executor): This variant of the
handleAsync()
method allows you to specify a custom executor for executing the handler asynchronously.
Here’s an example demonstrating how to handle exceptions using the CompletableFuture
in Java:
import java.util.concurrent.CompletableFuture;
public class CompletableFutureExceptionHandlingExample {
public static void main(String[] args) {
// Create a CompletableFuture representing an asynchronous computation
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
// Simulate a computation that throws an exception
throw new RuntimeException("An error occurred during computation.");
});
// Handling exceptions using exceptionally()
CompletableFuture<Integer> handledFuture = future.exceptionally(ex -> {
System.out.println("Exception occurred: " + ex.getMessage());
return -1; // Default value to return on exception
});
// Handling exceptions using handle()
CompletableFuture<Integer> handledFutureWithHandle = future.handle((result, ex) -> {
if (ex != null) {
System.out.println("Exception occurred: " + ex.getMessage());
return -1; // Default value to return on exception
}
return result;
});
// Printing the results after handling exceptions
handledFuture.thenAccept(result -> System.out.println("Result after handling (exceptionally): " + result));
handledFutureWithHandle.thenAccept(result -> System.out.println("Result after handling (handle): " + result));
}
}
In this example:
- We create a
CompletableFuture
namedfuture
usingsupplyAsync()
, simulating a computation that throws an exception. - We use the
exceptionally()
method to handle the exception. Theexceptionally()
method takes a function that gets executed if an exception occurs in the original future. The function can provide a default value or alternative result. - We use the
handle()
method to handle the exception. Thehandle()
method takes a function that gets executed regardless of whether an exception occurred or not. The function receives both the result and the exception, and it’s expected to return a result that considers the exception scenario. - The
thenAccept()
method is used to print the results after handling exceptions for bothhandledFuture
andhandledFutureWithHandle
.
Combining Results in CompletableFuture
You can combine the results of multiple CompletableFuture
instances using methods like thenCombine
, thenCombineAsync
etc.
Java Code
import java.util.concurrent.CompletableFuture;
public class CompletableFutureCombineExample {
public static void main(String[] args) {
// Create two CompletableFuture instances with simulated asynchronous computations
CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> 10);
CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> 20);
// Combine results using thenCombine (synchronously)
CompletableFuture<Integer> combinedFutureSync = future1.thenCombine(
future2,
(result1, result2) -> result1 + result2
);
// Combine results using thenCombineAsync (asynchronously)
CompletableFuture<Integer> combinedFutureAsync = future1.thenCombineAsync(
future2,
(result1, result2) -> result1 + result2
);
// Print the combined results
combinedFutureSync.thenAccept(result -> System.out.println("Combined sync result: " + result));
combinedFutureAsync.thenAccept(result -> System.out.println("Combined async result: " + result));
// Block the main thread for demonstration purposes
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
In this example, both thenCombine
and thenCombineAsync
methods are used to combine the results of the two futures. The synchronous combination result is printed using combinedFutureSync
, and the asynchronous combination result is printed using combinedFutureAsync
. The output will show the combined results obtained using both synchronous and asynchronous methods.
Using thenCompose Method of CompletableFuture
The thenCompose
method in CompletableFuture
is used to chain asynchronous operations sequentially, where the result of the first operation is used as an input to the second operation, resulting in a new CompletableFuture
that represents the outcome of the second operation.
Here’s an example of how to use the thenCompose
method:
import java.util.concurrent.CompletableFuture;
public class ThenComposeExample {
public static void main(String[] args) {
CompletableFuture<String> firstFuture = CompletableFuture.supplyAsync(() -> "Hello");
// Using thenCompose to chain two operations sequentially
CompletableFuture<String> finalFuture = firstFuture.thenCompose(result ->
CompletableFuture.supplyAsync(() -> result + " World")
);
finalFuture.thenAccept(result -> System.out.println("Final Result: " + result));
}
}
In this example, we first create a CompletableFuture
named firstFuture
that supplies the string “Hello”. Then, we use the thenCompose
method to chain a second asynchronous operation, where the result of firstFuture
is used as input. The second operation supplies ” World” and appends it to the result from the first operation.
The final result is “Hello World”, and it’s printed using the thenAccept
callback.
Manually Complete CompletableFuture
The complete
method is used to manually complete a CompletableFuture
that hasn’t been completed yet. Once a CompletableFuture
is completed, either with a result or an exception, you cannot modify its state using the complete
method.
Here’s how you can use the complete
method on a CompletableFuture
that hasn’t been completed yet:
import java.util.concurrent.CompletableFuture;
public class CompletableFutureCompleteExample {
public static void main(String[] args) {
CompletableFuture<String> future = new CompletableFuture<>();
// Complete the future with a result
future.complete("Completed Result");
// Attempting to complete again will have no effect
future.complete("New Result");
// Accessing the result of the completed future
future.thenAccept(result -> System.out.println("Result: " + result));
// Block the main thread for demonstration purposes
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
In this example, we create a CompletableFuture
named future
without specifying a result initially. We then use the complete
method to manually complete the future with the result “Completed Result”. Any subsequent attempts to complete the future again will have no effect.
The thenAccept
callback will print the completed result (“Completed Result”).
Cancel a CompletableFuture
The cancel
method in CompletableFuture
is used to attempt to cancel the associated asynchronous computation represented by the CompletableFuture
. It is a way to signal that the computation should be interrupted or stopped if it has not completed yet.
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
public class CompletableFutureCancelExample {
public static void main(String[] args) {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
try {
// Simulate a long-running computation
Thread.sleep(2000);
return "Result after computation";
} catch (InterruptedException e) {
return "Computation interrupted";
}
});
// Cancel the CompletableFuture after a certain delay
boolean wasCancelled = future.cancel(true);
try {
// Attempting to get the result will throw a CancellationException
String result = future.get();
} catch (CancellationException e) {
System.out.println("Future was cancelled.");
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
System.out.println("Was the future successfully cancelled? " + wasCancelled);
}
}
In this example, we create a CompletableFuture
named future
that simulates a long-running computation using supplyAsync
. We then use the cancel
method to attempt to cancel the future after a certain delay.
When attempting to get the result of the cancelled future using the get
method, a CancellationException
is thrown. The boolean value returned by the cancel
method indicates whether the cancellation attempt was successful.
Future vs CompletableFuture
Feature | Future | CompletableFuture |
---|---|---|
Asynchronous | Yes, represents a result of an asynchronous computation. | Yes, designed for more advanced asynchronous programming. |
Composition | Limited composition support, can be combined using ExecutorService and Callable . | Strong composition support using methods like thenApply , thenCompose , etc. |
Exception Handling | Limited support, exceptions need to be caught explicitly. | Enhanced exception handling with methods like handle , exceptionally , etc. |
Callbacks | Not directly supported, needs polling or other mechanisms. | Supports callback chaining using methods like thenRun , thenAccept , etc. |
Result Transformation | Requires manual transformation in code. | Offers methods for transforming and combining results, making code cleaner. |
Combining Results | Limited support for combining multiple Future instances. | Provides methods like allOf , anyOf , and combine to combine results. |
Cancellation | Limited cancellation support using cancel method. | Enhanced cancellation support with cancel , completeExceptionally , etc. |
Timeouts | Limited timeout support using get with a timeout parameter. | Supports explicit timeouts with orTimeout method. |
Explicit Completion | Requires manual setting of the result using set methods. | Provides methods like complete , completeExceptionally for explicit completion. |
Asynchronous Exceptions | Cannot capture exceptions thrown asynchronously. | Allows handling of exceptions thrown asynchronously through methods like handle and exceptionally . |
Complex Workflows | Difficult to manage complex workflows. | Easier management of complex workflows using chaining and composition. |
Java Version | Introduced in Java 5. | Introduced in Java 8. |