The one with CompletableFuture

CompletableFuture, introduced in Java 8, provides a powerful and flexible mechanism for asynchronous programming, allowing you to write non-blocking code that can significantly improve performance and resource utilization. Let’s dive into how you can leverage CompletableFuture to build more reactive applications.

What is Asynchronous Programming and Why CompletableFuture?

Traditionally, Java code executes synchronously – each line of code waits for the previous one to complete. For time-consuming operations like I/O calls (network requests, database queries, file system access), this can lead to blocked threads, making your application sluggish or unresponsive. Asynchronous programming allows your application to initiate a long-running task and then continue with other work without waiting for that task to finish. When the task eventually completes, your application can process its result or handle any errors.

Before CompletableFuture, Java offered Future, but it was quite limited. Retrieving the result was blocking (get()), and there was no straightforward way to compose multiple asynchronous operations or handle callbacks efficiently.

CompletableFuture extends Future and implements CompletionStage, offering a rich API for:

  • Explicitly completing futures.
  • Defining asynchronous tasks.
  • Chaining dependent operations.
  • Combining results from multiple futures.
  • Robust exception handling.

Creating a CompletableFuture

1. runAsync() – For tasks without a return value:

public class RunAsyncExample {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("Main thread: " + Thread.currentThread().getName());

        CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
            try {
                TimeUnit.SECONDS.sleep(2);
                System.out.println("Async task executed by: " + Thread.currentThread().getName());
            } catch (InterruptedException e) {
                throw new IllegalStateException(e);
            }
        });

        System.out.println("Main thread continues execution...");
        // Do other work here

        future.join(); // Wait for the async task to complete before main thread exits
        System.out.println("Async task finished.");
    }
}

By default, these tasks run on the global ForkJoinPool.commonPool(). You can also provide your own Executor:

public class RunAsyncWithExecutorExample {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(2);
        System.out.println("Main thread: " + Thread.currentThread().getName());

        CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
            try {
                TimeUnit.SECONDS.sleep(2);
                System.out.println("Async task with custom executor: " + Thread.currentThread().getName());
            } catch (InterruptedException e) {
                throw new IllegalStateException(e);
            }
        }, executor);

        System.out.println("Main thread continues...");
        future.join();
        System.out.println("Async task finished.");
        executor.shutdown();
    }
}

2. supplyAsync() – For tasks that return a value:

public class SupplyAsyncExample {
    public static void main(String[] args) {
        System.out.println("Main thread: " + Thread.currentThread().getName());

        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            try {
                TimeUnit.SECONDS.sleep(2);
                System.out.println("Supplier task executed by: " + Thread.currentThread().getName());
                return "Hello from CompletableFuture!";
            } catch (InterruptedException e) {
                throw new IllegalStateException(e);
            }
        });

        System.out.println("Main thread continues execution...");

        // Get the result (blocking)
        try {
            String result = future.get(); // or future.join()
            System.out.println("Result: " + result);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("Async task finished and result retrieved.");
    }
}

Table summarizing the key CompletableFuture methods:

MethodDetailedReturnsKey Characteristics
supplyAsync(Supplier<U> supplier, [Executor exec])Asynchronously executes a block of code (the Supplier) that is expected to produce a result of type U. The task is typically offloaded to a thread from a thread pool.CompletableFuture<U>Runs on ForkJoinPool.commonPool() by default, or a specified Executor. The returned future will eventually hold the supplier’s result.
runAsync(Runnable runnable, [Executor exec])Asynchronously executes a block of code (the Runnable) that does not produce a result (i.e., its outcome is void). The task is offloaded to a thread.CompletableFuture<Void>Runs on ForkJoinPool.commonPool() by default, or a specified Executor. The future completes with null when the runnable finishes.
completedFuture(U value)Creates and returns a new CompletableFuture that is already in a completed state, holding the provided value.CompletableFuture<U>Useful for initiating a chain of asynchronous operations with an immediately available value or for testing purposes.
thenApply(Function<T, U> fn, [Executor exec])Takes the result (type T) of the current CompletableFuture (once it completes) and applies the given Function (fn) to transform it into a new value (type U).CompletableFuture<U> (new result)Async versions run fn in a pool thread. Non-Async might use the thread that completed the previous stage if immediately available.
thenAccept(Consumer<T> action, [Executor exec])Takes the result (type T) of the current CompletableFuture (once it completes) and passes it to the given Consumer (action) to perform some side effect (e.g., printing, saving). Does not return a value from the action.CompletableFuture<Void>Async versions run action in a pool thread. Useful when you need to act on a result but not transform it further.
thenRun(Runnable action, [Executor exec])Executes the given Runnable (action) after the current CompletableFuture completes. The action does not receive the result of the previous stage.CompletableFuture<Void>Async versions run action in a pool thread. Useful for cleanup tasks or notifications post-completion.
thenCompose(Function<T, CompletionStage<U>> fn, [Executor exec])Used for chaining dependent asynchronous operations. When the current future (type T) completes, fn is applied to its result. fn itself must return a CompletionStage<U> (typically another CompletableFuture). This method flattens the result, avoiding CompletableFuture<CompletableFuture<U>>.CompletableFuture<U> (from fn)Analogous to flatMap in Streams. Async versions run fn in a pool thread. Indispensable for sequential async workflows.
thenCombine(CompletionStage<U> other, BiFunction<T,U,V> fn, [Executor exec])Waits for both the current CompletableFuture (result type T) and another CompletionStage (other, result type U) to complete. Then, their results are passed to the BiFunction (fn) to produce a combined result (type V).CompletableFuture<V> (combined)Async versions run fn in a pool thread. Used for parallel independent tasks whose results need to be merged.
thenAcceptBoth(CompletionStage<U> other, BiConsumer<T,U> action, [Executor exec])Waits for both the current CompletableFuture and another CompletionStage (other) to complete. Then, their results are passed to the BiConsumer (action) for a combined side effect. No result is returned from the action.CompletableFuture<Void>Async versions run action in a pool thread.
runAfterBoth(CompletionStage<?> other, Runnable action, [Executor exec])Waits for both the current CompletableFuture and another CompletionStage (other) to complete. Then, executes the Runnable (action). The action does not receive results from the futures.CompletableFuture<Void>Async versions run action in a pool thread.
allOf(CompletableFuture<?>... cfs)Returns a new CompletableFuture<Void> that completes only when all of the CompletableFutures provided in the cfs array have completed. If any of the input futures complete exceptionally, the resulting allOf future also completes exceptionally.CompletableFuture<Void>The result of this future is Void. To get individual results, you typically chain thenApply and access the original futures (which are now guaranteed to be done).
anyOf(CompletableFuture<?>... cfs)Returns a new CompletableFuture<Object> that completes as soon as any one of the CompletableFutures provided in the cfs array completes. The result of this new future is the result of the first one to complete.CompletableFuture<Object>If the first completed future completes exceptionally, anyOf also completes exceptionally. The result type is Object and may need casting.
exceptionally(Function<Throwable, T> fn)Provides a way to recover from an exception thrown in any preceding stage of the CompletableFuture chain. If an exception occurs, the Function (fn) is called with the Throwable, and its return value (type T) becomes the result of the new future.CompletableFuture<T> (original or fallback)If the preceding stage completed normally, this exceptionally stage is skipped, and the original result is propagated.
handle(BiFunction<T, Throwable, U> fn, [Executor exec])Provides a way to process the outcome of a preceding stage, regardless of whether it completed normally (with a result of type T) or exceptionally (with a Throwable). The BiFunction (fn) receives both (one will be null) and produces a new result (type U).CompletableFuture<U> (new result)This stage is always executed. Async versions run fn in a pool thread. Offers a comprehensive way to deal with outcomes.
whenComplete(BiConsumer<T, Throwable> action, [Executor exec])Executes the given BiConsumer (action) when the preceding CompletableFuture completes. The action receives the result (type T, or null if exceptional) and the Throwable (or null if normal). It does not modify the result or recover from exceptions.CompletableFuture<T> (original)Primarily used for side effects like logging or cleanup, as it passes through the original outcome. Async versions run action in a pool thread.
complete(T value)Manually completes this CompletableFuture with the given value, if it has not already been completed. Triggers any dependent stages.booleanReturns true if this call successfully transitioned the future to a completed state; false if it was already completed.
completeExceptionally(Throwable ex)Manually completes this CompletableFuture exceptionally with the given Throwable (ex), if it has not already been completed. Triggers any dependent stages.booleanReturns true if this call successfully transitioned the future to an exceptionally completed state.
isDone()Returns true if this CompletableFuture has completed in any fashion: normally, exceptionally, or via cancellation. Otherwise, returns false.booleanNon-blocking check of the future’s state.
isCompletedExceptionally()Returns true if this CompletableFuture completed due to an unhandled exception.booleanNon-blocking.
isCancelled()Returns true if this CompletableFuture was cancelled before it completed normally.booleanNon-blocking.
get() / get(long timeout, TimeUnit unit)Waits (blocks the calling thread) if necessary for the computation to complete, and then retrieves its result. The timed version waits only for the specified duration.VThrows checked exceptions: InterruptedException (if the waiting thread is interrupted), ExecutionException (if the computation threw an exception), TimeoutException (for timed get).
join()Waits (blocks the calling thread) if necessary for the computation to complete, and then retrieves its result. Similar to get(), but throws unchecked exceptions.VThrows an unchecked CompletionException if the computation threw an exception, or CancellationException if cancelled.
getNow(V valueIfAbsent)Returns the result if this CompletableFuture is already completed. Otherwise, it immediately returns the provided valueIfAbsent without blocking.VUseful for polling or providing a default when a result isn’t ready.
orTimeout(long timeout, TimeUnit unit) (Java 9+)If this CompletableFuture is not completed before the given timeout duration, it is completed exceptionally with a TimeoutException.CompletableFuture<T>Returns a new CompletableFuture that incorporates this timeout behavior.
completeOnTimeout(T value, long timeout, TimeUnit unit) (Java 9+)If this CompletableFuture is not completed before the given timeout duration, it is completed with the provided default value.CompletableFuture<T>Returns a new CompletableFuture that incorporates this default-on-timeout behavior.

Leave a Reply

Your email address will not be published. Required fields are marked *