CompletableFuture in Java 8

In this article, we will explore CompletableFuture in Java 8 in-depth. Let’s get started.

CompletableFuture in Java 8

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:

  1. Asynchronous Execution: CompletableFuture provides a way to perform asynchronous computations, allowing you to start and manage tasks that run concurrently.
  2. Chaining Operations: You can chain multiple asynchronous operations together using methods like thenApply, thenAccept, and thenCompose, creating a sequence of actions to be performed when the previous one completes.
  3. Non-Blocking Exception Handling: CompletableFuture offers methods like exceptionally, handle, and whenComplete to handle exceptions that might occur during asynchronous computations.
  4. Combining Results: You can combine the results of multiple CompletableFuture instances using methods like thenCombine, thenCombineAsync, and thenCompose.
  5. Asynchronous Execution Control: Methods like thenRun, thenRunAsync, and thenRunAsync allow you to specify tasks to run after the main computation completes, irrespective of the result.
  6. Completing Futures: You can explicitly complete a CompletableFuture using methods like complete, completeExceptionally, and obtrudeValue to set the result or exception.
  7. Timeouts: The orTimeout method allows you to set a timeout for a CompletableFuture, causing it to complete exceptionally if it takes too long to finish.
  8. Support for Streams: Java 9 and later versions offer integration with the CompletableFuture API, allowing you to convert streams into asynchronous computations using CompletableFuture methods.
  9. Cancellation: You can cancel a CompletableFuture using the cancel method, which can propagate cancellation to dependent futures.
  10. Parallelism and Concurrency: CompletableFuture enables better utilization of multicore processors by allowing tasks to be executed concurrently.
  11. Flexible Result Handling: CompletableFuture provides various methods to retrieve results, including blocking methods like get and non-blocking methods like join.
  12. Executor Customization: You can choose different executors to control where and how tasks are executed, improving resource management and performance.

Important methods of CompletableFuture

MethodDescription
thenApplyApplies a function to the result of the current CompletableFuture, producing a new CompletableFuture.
thenAcceptAccepts a consumer function that operates on the result, producing a new CompletableFuture with Void result.
thenRunExecutes a runnable action after the current CompletableFuture completes, producing a new CompletableFuture.
thenComposeChains a new CompletableFuture by applying a function that returns a CompletableFuture to the current result.
thenCombineCombines two CompletableFuture results using a function, producing a new CompletableFuture with the result.
thenEitherReturns a new CompletableFuture that completes when either of two given CompletableFuture completes.
thenApplyAsyncApplies a function asynchronously to the result using the default executor.
thenAcceptAsyncPerforms an asynchronous action on the result using the default executor.
thenRunAsyncExecutes an asynchronous runnable action using the default executor.
thenComposeAsyncApplies a function asynchronously using the default executor and returns a new CompletableFuture.
thenCombineAsyncCombines two CompletableFuture asynchronously using the default executor.
thenEitherAsyncReturns a new CompletableFuture that completes asynchronously when either of two given CompletableFuture completes.
exceptionallyHandles exceptions and produces a new CompletableFuture with a replacement value or exception.
handleHandles exceptions or the result with a function, producing a new CompletableFuture.
whenCompleteExecutes an action on completion (either success or failure) and returns a new CompletableFuture.
thenApplyToEitherApplies a function to the result of the first completed CompletableFuture among two.
thenComposeToEitherChains a new CompletableFuture by applying a function to the result of the first completed CompletableFuture.
allOfCombines an array of CompletableFuture into a single CompletableFuture that completes when all complete.
anyOfReturns a new CompletableFuture that completes when any of the given CompletableFuture completes.
runAsyncCreates a new CompletableFuture and schedules a runnable action to execute asynchronously.
supplyAsyncCreates a new CompletableFuture and schedules a supplier action to execute asynchronously.
completeManually completes a CompletableFuture with a given value.
cancelAttempts to cancel the execution of the CompletableFuture.
isDoneChecks if the CompletableFuture is completed, whether normally, exceptionally, or via cancellation.
getRetrieves 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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. exceptionallyAsync(Function<Throwable, T> handler): Similar to exceptionally(), this method executes the exception handler in a different thread (asynchronously) using the default executor.
  6. whenCompleteAsync(BiConsumer<T, Throwable> action): Similar to whenComplete(), this method executes the action asynchronously using the default executor.
  7. 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:

  1. We create a CompletableFuture named future using supplyAsync(), simulating a computation that throws an exception.
  2. We use the exceptionally() method to handle the exception. The exceptionally() 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.
  3. We use the handle() method to handle the exception. The handle() 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.
  4. The thenAccept() method is used to print the results after handling exceptions for both handledFuture and handledFutureWithHandle.

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

FeatureFutureCompletableFuture
AsynchronousYes, represents a result of an asynchronous computation.Yes, designed for more advanced asynchronous programming.
CompositionLimited composition support, can be combined using ExecutorService and Callable.Strong composition support using methods like thenApply, thenCompose, etc.
Exception HandlingLimited support, exceptions need to be caught explicitly.Enhanced exception handling with methods like handle, exceptionally, etc.
CallbacksNot directly supported, needs polling or other mechanisms.Supports callback chaining using methods like thenRun, thenAccept, etc.
Result TransformationRequires manual transformation in code.Offers methods for transforming and combining results, making code cleaner.
Combining ResultsLimited support for combining multiple Future instances.Provides methods like allOf, anyOf, and combine to combine results.
CancellationLimited cancellation support using cancel method.Enhanced cancellation support with cancel, completeExceptionally, etc.
TimeoutsLimited timeout support using get with a timeout parameter.Supports explicit timeouts with orTimeout method.
Explicit CompletionRequires manual setting of the result using set methods.Provides methods like complete, completeExceptionally for explicit completion.
Asynchronous ExceptionsCannot capture exceptions thrown asynchronously.Allows handling of exceptions thrown asynchronously through methods like handle and exceptionally.
Complex WorkflowsDifficult to manage complex workflows.Easier management of complex workflows using chaining and composition.
Java VersionIntroduced in Java 5.Introduced in Java 8.

Leave a Reply

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