What Are Some Practical Use Cases for the CompletableFuture Class in Java?

What Are Some Practical Use Cases for the CompletableFuture Class in Java?

The CompletableFuture class is a powerful tool in Java for dealing with asynchronous programming and handling concurrency. It was introduced in Java 8 to simplify the use of futures and promises, providing an easier way to write asynchronous code. In this article, we will explore various use cases for the CompletableFuture class in Java, backed by detailed code examples.

1. Handling Asynchronous Tasks

One of the most common use cases for CompletableFuture is to handle asynchronous tasks. In traditional programming, tasks are executed sequentially. However, with asynchronous programming, tasks can be executed concurrently, which can lead to significant performance improvements, especially in I/O-bound applications.

CompletableFuture allows you to execute tasks asynchronously while still being able to process the results when they become available. This can be helpful in web services, microservices, or applications that rely on external resources like databases or APIs.

Example: Asynchronous Task Execution

import java.util.concurrent.CompletableFuture;

public class AsyncTaskExample {
    public static void main(String[] args) {
        // Create a CompletableFuture to execute an asynchronous task
        CompletableFuture future = CompletableFuture.runAsync(() -> {
            try {
                System.out.println("Task started on thread: " + Thread.currentThread().getName());
                Thread.sleep(2000); // Simulate a time-consuming task
                System.out.println("Task completed.");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        // Wait for the task to complete
        future.join();
        System.out.println("Main thread continues...");
    }
}

In this example, we create a CompletableFuture that executes a task asynchronously using the runAsync method. The main thread continues execution while the asynchronous task runs in the background.

2. Handling Complex Workflows with thenApply and thenCombine

Another important use case is managing complex workflows that involve multiple stages of asynchronous processing. You can chain multiple actions using methods like thenApply and thenCombine. These methods help you compose a sequence of asynchronous tasks in a declarative manner.

thenApply is used when you want to transform the result of an asynchronous task, while thenCombine is used when you need to combine the results of two independent asynchronous tasks.

Example: Chaining Tasks Using thenApply

import java.util.concurrent.CompletableFuture;

public class ChainingExample {
    public static void main(String[] args) {
        CompletableFuture future = CompletableFuture.supplyAsync(() -> {
            return 20;
        });

        future.thenApply(result -> {
            return result * 2;
        }).thenApply(result -> {
            return result + 10;
        }).thenAccept(result -> {
            System.out.println("Final Result: " + result);
        });

        // Wait for the task to complete
        future.join();
    }
}

In this example, we first start an asynchronous task that supplies an integer (20). Then, we apply a transformation where the result is multiplied by 2 and later added to 10. The final result is printed when all the transformations are complete.

Example: Using thenCombine for Combining Results

import java.util.concurrent.CompletableFuture;

public class CombineExample {
    public static void main(String[] args) {
        CompletableFuture future1 = CompletableFuture.supplyAsync(() -> {
            return 10;
        });

        CompletableFuture future2 = CompletableFuture.supplyAsync(() -> {
            return 20;
        });

        future1.thenCombine(future2, (result1, result2) -> {
            return result1 + result2;
        }).thenAccept(result -> {
            System.out.println("Combined Result: " + result);
        });

        // Wait for the tasks to complete
        future1.join();
        future2.join();
    }
}

Here, two independent tasks are executed asynchronously. The results of both tasks are combined using thenCombine, and the combined result is printed once both tasks are complete.

3. Handling Timeouts and Exceptions

In real-world applications, tasks may fail or take too long to complete. With CompletableFuture, you can easily handle timeouts and exceptions to make your program more resilient.

CompletableFuture provides methods like completeExceptionally and orTimeout to handle exceptions and specify timeouts for asynchronous tasks. This can be useful for handling cases where a task might not finish in a reasonable amount of time.

Example: Handling Timeouts and Exceptions

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;

public class TimeoutExample {
    public static void main(String[] args) {
        CompletableFuture future = CompletableFuture.supplyAsync(() -> {
            try {
                TimeUnit.SECONDS.sleep(3); // Simulate long-running task
                return "Task Completed";
            } catch (InterruptedException e) {
                throw new RuntimeException("Task interrupted");
            }
        }).orTimeout(2, TimeUnit.SECONDS)
          .exceptionally(ex -> "Task Timed Out");

        future.thenAccept(System.out::println); // Print the result

        // Wait for the task to complete
        future.join();
    }
}

In this example, we set a timeout of 2 seconds for the task. If the task doesn’t finish within that time, it will throw an exception, and we handle it using the exceptionally method, which returns a fallback message.

4. Combining Multiple Asynchronous Tasks with allOf and anyOf

When you need to wait for multiple asynchronous tasks to complete, CompletableFuture provides two useful methods: allOf and anyOf. These methods help you manage scenarios where you need to wait for multiple tasks or proceed when at least one task completes.

Example: Using allOf to Wait for Multiple Tasks

import java.util.concurrent.CompletableFuture;

public class AllOfExample {
    public static void main(String[] args) {
        CompletableFuture future1 = CompletableFuture.runAsync(() -> {
            try {
                Thread.sleep(1000);
                System.out.println("Task 1 complete");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        CompletableFuture future2 = CompletableFuture.runAsync(() -> {
            try {
                Thread.sleep(2000);
                System.out.println("Task 2 complete");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        CompletableFuture allOfFuture = CompletableFuture.allOf(future1, future2);

        // Wait for all tasks to complete
        allOfFuture.join();
        System.out.println("All tasks completed.");
    }
}

In this example, we wait for both future1 and future2 to complete before printing the final message. The allOf method ensures that the program waits for both tasks.

Example: Using anyOf to Wait for Any Task to Complete

import java.util.concurrent.CompletableFuture;

public class AnyOfExample {
    public static void main(String[] args) {
        CompletableFuture future1 = CompletableFuture.runAsync(() -> {
            try {
                Thread.sleep(1000);
                System.out.println("Task 1 complete");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        CompletableFuture future2 = CompletableFuture.runAsync(() -> {
            try {
                Thread.sleep(500);
                System.out.println("Task 2 complete");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        CompletableFuture anyOfFuture = CompletableFuture.anyOf(future1, future2);

        // Wait for any task to complete
        anyOfFuture.join();
        System.out.println("At least one task completed.");
    }
}



In this example, we use anyOf to proceed as soon as either future1 or future2 completes. This is useful when you want to continue as soon as possible, even if not all tasks have finished.

Conclusion

The CompletableFuture class in Java is an essential tool for dealing with asynchronous tasks and concurrency. With methods like thenApply, thenCombine, allOf, and anyOf, it simplifies the development of complex workflows and handling concurrent tasks. By leveraging its power, Java developers can build more efficient and responsive applications.

Please follow and like us:

Leave a Comment