Working with Java 8’s CompletableFuture for Asynchronous Programming

Published: November 20, 2016


Introduction

Asynchronous programming has become an essential feature in modern applications, especially when dealing with IO-bound tasks or tasks that involve waiting for external systems (like databases or APIs). In Java 8, the CompletableFuture class was introduced to simplify asynchronous programming.

In this tutorial, we will explore:

  • What CompletableFuture is and how it works.
  • How to create and manage asynchronous tasks.
  • How to combine multiple asynchronous tasks and handle their results.

By the end of this tutorial, you’ll understand how to use CompletableFuture for writing more efficient and scalable Java applications.


What is CompletableFuture?

CompletableFuture is part of the java.util.concurrent package and represents a future result of an asynchronous computation. It is a non-blocking alternative to Future and ExecutorService. The CompletableFuture class allows you to:

  • Run tasks asynchronously.
  • Chain multiple tasks together.
  • Combine results from multiple tasks.

It provides powerful methods for both composing and combining asynchronous computations in a more readable and structured way.


Basic Usage of CompletableFuture

Let’s start with a basic example that shows how to create and run an asynchronous task using CompletableFuture:

import java.util.concurrent.*;

public class CompletableFutureExample {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        // Create an asynchronous task
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
            try {
                Thread.sleep(2000); // Simulate long-running task
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return 42; // Return a result
        });

        // Wait for the result (blocking)
        Integer result = future.get(); // This blocks until the task completes
        System.out.println("Result: " + result);
    }
}

Explanation:

  • We create a CompletableFuture that runs an asynchronous task using the supplyAsync() method.
  • The get() method is called to block and wait for the result of the asynchronous computation.

Output:

Result: 42

Chaining Tasks with CompletableFuture

One of the powerful features of CompletableFuture is the ability to chain multiple tasks together using methods like thenApply()thenAccept(), or thenCompose(). Chaining tasks allows you to process the result of one task and pass it to another task without blocking the main thread.

Example: Chaining Multiple Tasks

import java.util.concurrent.*;

public class CompletableFutureExample {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        // Start an asynchronous task
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
            try {
                Thread.sleep(1000); // Simulate task
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return 10;
        });

        // Chain another task to process the result
        CompletableFuture<Integer> resultFuture = future.thenApplyAsync(result -> {
            System.out.println("Processing the result: " + result);
            return result * 2;
        });

        // Chain another task
        resultFuture.thenAcceptAsync(result -> {
            System.out.println("Final result after processing: " + result);
        });

        // Wait for all tasks to complete
        resultFuture.get();
    }
}

Explanation:

  • We first use supplyAsync() to start an asynchronous task.
  • The thenApplyAsync() method is used to process the result of the previous task and return a new result.
  • The thenAcceptAsync() method is used to perform an action with the final result (in this case, printing it).

Output:

Processing the result: 10
Final result after processing: 20

Handling Errors in CompletableFuture

In asynchronous programming, it’s essential to handle errors properly. CompletableFuture provides several methods, such as exceptionally() and handle(), to handle exceptions and recover from errors gracefully.

Example: Handling Exceptions

import java.util.concurrent.*;

public class CompletableFutureExample {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        // Start an asynchronous task that throws an exception
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
            throw new RuntimeException("Something went wrong!");
        });

        // Handle the exception
        CompletableFuture<Integer> resultFuture = future.exceptionally(ex -> {
            System.out.println("Error occurred: " + ex.getMessage());
            return -1; // Return a default value
        });

        // Wait for the result
        Integer result = resultFuture.get();
        System.out.println("Result after handling exception: " + result);
    }
}

Explanation:

  • In this example, the task throws an exception, but we use exceptionally() to catch the exception and return a default value (-1).

Output:

Error occurred: Something went wrong!
Result after handling exception: -1

Combining Multiple CompletableFuture Tasks

Sometimes you need to combine results from multiple asynchronous tasks. CompletableFuture provides methods like thenCombine()allOf(), and anyOf() to help you with such cases.

Example: Combining Results Using thenCombine()

import java.util.concurrent.*;

public class CompletableFutureExample {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        // First asynchronous task
        CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {
            return 10;
        });

        // Second asynchronous task
        CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> {
            return 20;
        });

        // Combine results from both tasks
        CompletableFuture<Integer> combinedFuture = future1.thenCombine(future2, (result1, result2) -> {
            return result1 + result2;
        });

        // Wait for the result
        Integer result = combinedFuture.get();
        System.out.println("Combined result: " + result);
    }
}

Explanation:

  • We create two asynchronous tasks (future1 and future2).
  • The thenCombine() method is used to combine the results of both tasks. The lambda expression adds the results together.

Output:

Combined result: 30

Conclusion

CompletableFuture is a powerful tool for asynchronous programming in Java. It allows you to run tasks asynchronously, handle errors, and combine multiple tasks in a clean and readable manner.

In this tutorial, you learned how to:

  • Run asynchronous tasks using CompletableFuture.
  • Chain tasks together using thenApplyAsync()thenAcceptAsync().
  • Handle exceptions with exceptionally() and handle().
  • Combine results from multiple tasks using thenCombine().

In the next tutorial, we will dive into the usage of flatMap() in Java 8’s Streams API to handle more complex data structures.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

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