A Comprehensive Guide To Java Completable Future

    A Comprehensive Guide To Java Completable Future

    This article explores CompletableFuture in Java, a key feature introduced in Java 8 for simplifying asynchronous programming. It covers how CompletableFuture enables non-blocking execution, task composition, and parallel task handling, making it ideal for creating scalable, high-performance applications.

    default profile

    Shreya Adak

    February 14, 2025

    8 min read

    Introduction:#

    CompletableFuture is a class in Java's java.util.concurrent package that Java 8 brought in. This class has an impact on asynchronous programming. It lets you create code that doesn't block, runs at the same time, and responds to events in a way that's easier to read and more effective. You can finish it yourself, or it can finish on its own when the work is done. It gives you a Supplier instance as a lambda expression that does the math and gives back the answer.

    In Synchronous programming, each job runs one after the other. This means the program waits for one job to end before it starts the next one.

    Asynchronous programming lets tasks run out of order, allowing multiple operations to happen at the same time or without waiting or blocking. This approach boosts productivity, makes systems more responsive, and helps them grow, especially in apps that need to handle many tasks at once. If you're creating web servers, mobile apps, or apps that work with lots of data, asynchronous programming helps you build systems that are effective, quick, and able to handle growth.

    Evolution of Asynchronous Programming in Java#

    1. Pre-Java 5 (Manual Threads & Runnables)
      • Used Thread and Runnable to run tasks asynchronously.
      • Issues: Manual thread management, resource-heavy, no direct return values.
    2. Java 5 (Future and ExecutorService)
      • Introduced Future for handling async tasks with ExecutorService.
      • Issues: get() blocks execution, no easy chaining, and poor exception handling.
    3. Java 7 (Fork/Join Framework)
      • Optimized parallel execution for CPU-intensive tasks.
      • Issues: Complex API, not ideal for I/O operations.
    4. Java 8 (CompletableFuture)
      • Fully non-blocking, supports method chaining (thenApply(), thenCombine()).
      • Built-in exception handling (exceptionally(), handle()).
      • Allows combining multiple async tasks efficiently.
      • Solves all previous limitations, making Java async programming more scalable.

    Future in Java: Future<T> is an interface of java from Java's java.util.concurrent package which is used to handle asynchronous computations. It checks that the task is done properly or canceled.

    Key Features of Completable:#

    Asynchronous Task Management: Allows tasks to run the main program independently without waiting or blocking.

    Flexible Completion Handling: Provides methods (e.g. thenRun, thenApply, and thenAccept ) to handle task results once completed.

    Exception Handling: Enables to manage exceptions (that occur at the time of asynchronous execution).

    Task Combination: Supports to combine multiple tasks using methods (e.g. thenCombine and allOf).

    Implementation of CompletableFuture in Java:#

    CompletableFuture allows for powerful, non-blocking asynchronous programming in Java. It provides:

    • Easy chaining of tasks.
    • Exception handling for asynchronous operations.
    • Handling multiple asynchronous tasks (using allOf(), anyOf(), and thenCombine()).
    • Custom execution via Executors.
    • Efficient parallel task execution.

    By leveraging CompletableFuture, Java developers can build more responsive, scalable, and efficient applications.

    1. Basic CompletableFuture Example using: Creating a CompletableFuture Class#

    Create an object/instance of the CompletableFuture class using *supplyAsync*() which is a supplier. This supplier provides a functional interface that only returns a value.

    import java.util.concurrent.CompletableFuture; public class LearnCompletableFutureClass { public static void main(String[] args) throws Exception{ CompletableFuture<String> objOfCompletableFuture = CompletableFuture.supplyAsync(()->{ return "Hello World"; }); System.out.println(objOfCompletableFuture.get()); } }

    Output:

    Basic CompletableFuture Example using: Creating a CompletableFuture Class

    2. Chaining Tasks with CompletableFuture#

    One of the most powerful features of CompletableFuture is chaining multiple tasks together. This is useful when you need to process results step-by-step or handle a series of dependent tasks.

    import java.util.concurrent.CompletableFuture; public class LearnCompletableFutureClass { public static void main(String[] args) throws Exception{ CompletableFuture<Integer> objOfCompletableFuture = CompletableFuture.supplyAsync(()->{ return 10; }); // Asynchronous tasks objOfCompletableFuture.thenApplyAsync(result -> { return result + 10; }) .thenApplyAsync(result -> { return result * 100; }) .thenAccept(result -> { System.out.println(result); }); } }
    • thenApplyAsync(): Allows chaining additional computations.
    • thenAccept(): Consumes the final result after all computations.

    Output:

    Chaining Tasks with CompletableFuture

    3. Handling Multiple CompletableFutures with allOf() , anyOf()#

    Multiple CompletableFuture tasks are executed parallelly. You can use anyOf() or allOf() to wait for the completion of multiple futures.

    import java.util.concurrent.CompletableFuture; public class LearnCompletableFutureClass { public static void main(String[] args) throws Exception{ CompletableFuture<Integer> task1 = CompletableFuture.supplyAsync(()->{ return 10; }); CompletableFuture<Integer> task2 = CompletableFuture.supplyAsync(()->{ return 20; }); CompletableFuture<Void> result1 = CompletableFuture.allOf(task1,task2); result1.thenRun(()->{ // this try-catch block for get() method try { System.out.println("Final result: " +(task1.get()+task2.get())); } catch (Exception e) { throw new RuntimeException(e); } }); CompletableFuture<Object> anyFuture = CompletableFuture.anyOf(task1, task2); anyFuture.thenAccept(result2 -> { System.out.println("First completed result: " + result2); }); } }
    • allOf(): Returns a new CompletableFuture when all tasks are done successfully otherwise if any of task fails then it throws an exception.
    • anyOf(): Returns when any one of the provided futures completes.

    Output:

     Handling Multiple CompletableFutures with allOf() , anyOf()

    4. Handling Exceptions in CompletableFuture#

    Exception handling in asynchronous tasks is easier with CompletableFuture. You can use methods like exceptionally(), handle(), or whenComplete() to deal with errors gracefully.

    import java.util.concurrent.CompletableFuture; public class LearnCompletableFutureClass { public static void main(String[] args) throws Exception{ CompletableFuture<Integer> task2 = CompletableFuture.supplyAsync(()->{ if(true) throw new RuntimeException("This is a Runtime exception"); // we need a true condition for throwing this exception return 20; //mandatory for return type integer }); task2.exceptionally(ex -> { System.out.println("Error: " + ex.getMessage()); return 0; // Return a default value in case of error }).thenAccept(result -> { System.out.println("Result: " + result); }); } }
    • exceptionally() : Handles exceptions by providing a fallback value.
    • handle() : Handles both success and failure, allowing transformation of results.
    • whenComplete() : Executes a callback after completion without modifying the result.

    Output:

    Handling Exceptions in CompletableFuture

    5. Combining Results from Multiple Futures Using thenCombine(),thenCompose() , thenApply(): Composing CompletableFuture#

    CompletableFuture can compose multiple asynchronous tasks.

    import java.util.concurrent.CompletableFuture; public class LearnCompletableFutureClass { public static void main(String[] args) throws Exception{ CompletableFuture<Integer> task1 = CompletableFuture.supplyAsync(()->{ return 20; }); CompletableFuture<Integer> task2 = CompletableFuture.supplyAsync(()->{ return 10; }); task2.thenCombine(task1,(task2Result,task1Result)-> task1Result + task2Result) .thenAccept(sum -> System.out.println("Final result of the sum: "+sum)); CompletableFuture<Integer> finalResult = task1.thenCompose(result1 -> task2.thenApply(result2 -> result1 * result2) ); finalResult.thenAccept(mul-> System.out.println("Final result of multiply: "+mul)); task1.thenApply(result->result / 5).thenAccept(div-> System.out.println("Final result of divide: "+div)); } }
    • thenCombine() : Combines results of two independent futures when both complete
    • thenCompose() : Chains dependent futures, using the result of the first to create a new future.
    • thenApply() : Transforms the result of a completed future into another value.

    Output:

    Combining Results from Multiple Futures Using thenCombine(),thenCompose() , thenApply(): Composing CompletableFuture

    6. Using runAsync() for Non-returning Tasks#

    Use runAsync() for tasks that do not return a value, such as logging or background operations.

    import java.util.concurrent.CompletableFuture; public class LearnCompletableFutureClass { public static void main(String[] args) throws Exception{ CompletableFuture<Void> future = CompletableFuture.runAsync(() -> { System.out.println("Task executed in the background"); }); } future.join(); }

    Output:

    Using runAsync() for Non-returning Tasks

    7. Using a Custom Executor with CompletableFuture#

    To control the concurrency and manage the number of threads, you can provide a custom executor for asynchronous tasks.

    import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class LearnCompletableFutureClass { public static void main(String[] args) throws Exception{ ExecutorService executorService = Executors.newFixedThreadPool(2); CompletableFuture<Void> future = CompletableFuture.runAsync(() -> { System.out.println("Task executed using custom executor"); }, executorService); future.join(); executorService.shutdown(); } }
    • ExecutorService manages asynchronous tasks by providing thread pooling and lifecycle control. It simplifies concurrent task execution, improving performance and resource management.

    Output:

    Using a Custom Executor with CompletableFuture

    Note: If you don't call future.get() or wait for the task to complete, the program might terminate before the background task finishes executing, depending on how the JVM handles the thread.

    Core Difference between Future and CompletableFuture: #

    Future is a simpler interface to asynchronous task management: with methods like get() that require the blocking of the current thread. Conversely, CompletableFuture is much more sophisticated and allows non-blocking operations for chaining tasks and better handling of exceptions. Use Future for straightforward, once-off tasks that can afford to block. Use CompletableFuture for more complex and chained non-blocking task workflows with good exception handling.

    FeaturesFuture<T> In JavaCompletableFuture<T> In Java
    IntroducedIn Java 5In Java 8
    Asynchronous ExecutionYes, but limitedYes, but without any limitation
    Non-blocking CallsNo, must use get() which blocksYes, supports non-blocking methods like thenApply(), thenAccept()
    Manual CompletionNo, only the computation can set the resultYes, can manually complete with complete() or completeExceptionally()
    Chaining OperationsNo, requires additional handlingYes, allows method chaining (thenApply(), thenCompose())
    Exception HandlingNo built-in exception handlingProvides exceptionally(), handle() for error handling
    Multiple Futures CombinationNot supported directlySupports combining multiple futures (thenCombine(), allOf(), anyOf())
    Thread PoolUses ExecutorService.submit()Uses ForkJoinPool but can accept custom executors

    Conclusion:#

    We explored the CompletableFuture in Java,  and how it is designed to simplify asynchronous programming. Non-blocking execution allows for the chaining of tasks and handling concurrent tasks. The exception handling, timeout management, and task interruption offered by CompletableFuture are simple and make it suitable for writing scalable and fast applications. Top Ranking Keywords: CompletableFuture Java, Asynchronous programming Java, Non-blocking execution, Java task composition, Scalable applications.

    Java
    Java 8 features
    Java Completable Future
    Non-blocking execution
    Java task composition

    More articles