Post

CompletableFuture in Java 8



Introduction

Java 8 introduced CompletableFuture, a powerful enhancement to the java.util.concurrent package, designed to simplify asynchronous programming and enable complex concurrent operations in a more readable and maintainable manner. This article will delve into the core features of CompletableFuture, its practical applications, and how it can be leveraged to improve concurrency in Java applications.

What is CompletableFuture?

CompletableFuture is a class that represents a future result of an asynchronous computation. It allows you to write non-blocking code by enabling you to attach callbacks that will be executed upon the completion of a computation. Unlike the traditional Future interface, CompletableFuture provides a more extensive API for handling asynchronous operations, including the ability to combine multiple futures, handle exceptions, and create complex asynchronous pipelines.

Key Features of CompletableFuture

  1. Asynchronous Computation
    You can start a computation asynchronously and define actions to be taken once the computation is complete.

  2. Chaining Operations
    CompletableFuture supports chaining multiple operations, enabling you to build complex asynchronous workflows in a declarative manner.

  3. Exception Handling
    It provides mechanisms to handle exceptions that occur during asynchronous computations.

  4. Combining Futures
    You can combine multiple CompletableFuture instances, waiting for all of them to complete or completing when any of them finishes.

Creating a CompletableFuture

You can create a CompletableFuture using various factory methods:

Creating a Completed CompletableFuture

1
2
CompletableFuture<String> completedFuture = CompletableFuture.completedFuture("Hello, World!");
completedFuture.thenAccept(System.out::println); // Output: Hello, World!

Creating a CompletableFuture with a Supplier

1
2
3
4
CompletableFuture.supplyAsync(() -> {
    // Simulate a long-running task
    return "Hello from CompletableFuture!";
}).thenAccept(System.out::println); // Output: Hello from CompletableFuture!

Chaining Operations

CompletableFuture allows you to chain multiple operations using methods like thenApply, thenAccept, and thenCompose.

Using thenApply

thenApply transforms the result of the CompletableFuture when it completes.

1
2
3
CompletableFuture.supplyAsync(() -> "Hello")
    .thenApply(result -> result + ", World!")
    .thenAccept(System.out::println); // Output: Hello, World!

Using thenAccept

thenAccept performs an action with the result when the CompletableFuture completes.

1
2
CompletableFuture.supplyAsync(() -> 42)
    .thenAccept(result -> System.out.println("Result is: " + result)); // Output: Result is: 42

Using thenCompose

thenCompose is used to chain multiple CompletableFuture instances. It is used when the next stage depends on the result of the previous stage and returns a new CompletableFuture.

1
2
3
CompletableFuture.supplyAsync(() -> "Hello")
    .thenCompose(result -> CompletableFuture.supplyAsync(() -> result + ", World!"))
    .thenAccept(System.out::println); // Output: Hello, World!

Exception Handling

CompletableFuture provides several methods for handling exceptions, including exceptionally and handle.

Using exceptionally

exceptionally allows you to recover from exceptions that occur during asynchronous computation.

1
2
3
4
5
6
7
8
CompletableFuture.supplyAsync(() -> {
    throw new RuntimeException("Something went wrong!");
})
.exceptionally(ex -> {
    System.out.println("Exception: " + ex.getMessage());
    return "Fallback result";
})
.thenAccept(System.out::println); // Output: Exception: Something went wrong! Fallback result

Using handle

handle allows you to handle both the result and the exception of a CompletableFuture.

1
2
3
4
5
6
7
8
9
10
11
CompletableFuture.supplyAsync(() -> {
    throw new RuntimeException("Error occurred!");
})
.handle((result, ex) -> {
    if (ex != null) {
        System.out.println("Exception: " + ex.getMessage());
        return "Fallback result";
    }
    return result;
})
.thenAccept(System.out::println); // Output: Exception: Error occurred! Fallback result

Combining Multiple Futures

CompletableFuture provides methods to combine multiple futures, such as allOf, anyOf, and thenCombine.

Using allOf

allOf allows you to wait for multiple futures to complete.

1
2
3
4
5
6
7
8
9
10
11
12
CompletableFuture<Void> future1 = CompletableFuture.supplyAsync(() -> {
    // Simulate a long-running task
    return "Task 1";
}).thenAccept(System.out::println);

CompletableFuture<Void> future2 = CompletableFuture.supplyAsync(() -> {
    // Simulate a long-running task
    return "Task 2";
}).thenAccept(System.out::println);

CompletableFuture<Void> allOf = CompletableFuture.allOf(future1, future2);
allOf.join(); // Waits for all futures to complete

Using anyOf

anyOf completes when any one of the provided futures completes.

1
2
3
4
5
6
7
8
9
10
11
12
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
    sleep(1000);
    return "Task 1";
});

CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
    sleep(500);
    return "Task 2";
});

CompletableFuture<Object> anyOf = CompletableFuture.anyOf(future1, future2);
anyOf.thenAccept(result -> System.out.println("First completed: " + result)); // Output: First completed: Task 2

Helper method for sleeping:

1
2
3
4
5
6
7
private static void sleep(int millis) {
    try {
        Thread.sleep(millis);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
}

Using thenCombine

thenCombine combines the results of two futures when both are complete.

1
2
3
4
5
CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> 5);
CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> 3);

future1.thenCombine(future2, (result1, result2) -> result1 + result2)
       .thenAccept(sum -> System.out.println("Sum: " + sum)); // Output: Sum: 8

Best Practices

  1. Avoid Blocking Calls
    Prefer non-blocking operations and avoid methods like get() which block until the future completes.

  2. Handle Exceptions Gracefully
    Use exceptionally and handle to manage exceptions and ensure that your application can recover from errors.

  3. Use CompletableFuture for Complex Pipelines
    When dealing with complex asynchronous workflows, use chaining and combining methods to create clear and manageable pipelines.

  4. Consider Using CompletableFuture for Parallel Processing
    Utilize CompletableFuture for parallel processing of tasks, especially when tasks are independent and can run concurrently.

Conclusion

CompletableFuture is a powerful tool in Java 8 that simplifies asynchronous programming and improves concurrency handling. By using CompletableFuture, developers can write more readable and maintainable code, efficiently manage asynchronous tasks, and build complex pipelines without resorting to nested callbacks. Mastering CompletableFuture can significantly enhance your ability to handle concurrency and improve the responsiveness of your Java applications.

© 2024 Java Tutorial Online. All rights reserved.