Asynchronous programming is crucial for developing responsive and efficient applications. In Java, the CompletableFuture
class introduced in Java 8 offers a powerful and flexible way to handle asynchronous programming. This tutorial aims to provide a comprehensive guide on how to use CompletableFuture
for non-beginners who are already familiar with Java but want to enhance their skills in asynchronous programming.
1. Introduction to CompletableFuture
CompletableFuture
is part of the java.util.concurrent
package and provides a way to write non-blocking, asynchronous code. It represents a future result of an asynchronous computation, allowing you to attach callbacks that will be executed upon its completion.
Key Features
- Non-blocking:
CompletableFuture
provides methods to attach callbacks that get executed without blocking the main thread. - Composability: It allows combining multiple futures in various ways, such as running them in parallel or chaining them sequentially.
- Exception Handling: It provides mechanisms to handle exceptions gracefully in asynchronous workflows.
2. Creating a CompletableFuture
Basic Creation and Completion
You can create a CompletableFuture
instance using its static factory methods or directly through its constructor. Here are some common ways to create and complete a CompletableFuture
.
import java.util.concurrent.CompletableFuture;
public class CompletableFutureExample {
public static void main(String[] args) {
// Creating a CompletableFuture
CompletableFuture<String> future = new CompletableFuture<>();
// Completing a CompletableFuture manually
future.complete("Hello, World!");
// Printing the result
future.thenAccept(result -> System.out.println(result));
}
}
Code language: Java (java)
Using Static Factory Methods
CompletableFuture
provides several static methods to create pre-completed futures or futures that complete asynchronously.
import java.util.concurrent.CompletableFuture;
public class CompletableFutureExample {
public static void main(String[] args) {
// Creating a completed future
CompletableFuture<String> completedFuture = CompletableFuture.completedFuture("Hello, World!");
completedFuture.thenAccept(System.out::println);
// Running a task asynchronously
CompletableFuture<Void> asyncFuture = CompletableFuture.runAsync(() -> {
// Simulate a long-running task
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Task completed");
});
// Waiting for the async task to complete
asyncFuture.join();
}
}
Code language: Java (java)
3. Combining Multiple CompletableFutures
Chaining Futures
One of the powerful features of CompletableFuture
is the ability to chain multiple futures together.
import java.util.concurrent.CompletableFuture;
public class CompletableFutureChainingExample {
public static void main(String[] args) {
CompletableFuture.supplyAsync(() -> "Hello")
.thenApply(result -> result + ", World!")
.thenAccept(System.out::println);
}
}
Code language: Java (java)
Combining Futures
You can combine multiple futures using methods like thenCombine
, thenCompose
, and allOf
.
import java.util.concurrent.CompletableFuture;
public class CompletableFutureCombiningExample {
public static void main(String[] args) {
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Hello");
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "World");
// Combining results of two futures
CompletableFuture<String> combinedFuture = future1.thenCombine(future2, (result1, result2) -> result1 + " " + result2);
combinedFuture.thenAccept(System.out::println);
}
}
Code language: Java (java)
Waiting for All Futures
If you need to wait for multiple futures to complete, you can use allOf
.
import java.util.concurrent.CompletableFuture;
public class CompletableFutureAllOfExample {
public static void main(String[] args) {
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Task 1");
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "Task 2");
CompletableFuture<String> future3 = CompletableFuture.supplyAsync(() -> "Task 3");
CompletableFuture<Void> allOf = CompletableFuture.allOf(future1, future2, future3);
allOf.thenRun(() -> {
System.out.println("All tasks completed");
}).join();
}
}
Code language: Java (java)
Handling Any Completed Future
You can also wait for any one of the futures to complete using anyOf
.
import java.util.concurrent.CompletableFuture;
public class CompletableFutureAnyOfExample {
public static void main(String[] args) {
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Task 1");
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "Task 2");
CompletableFuture<String> future3 = CompletableFuture.supplyAsync(() -> "Task 3");
CompletableFuture<Object> anyOf = CompletableFuture.anyOf(future1, future2, future3);
anyOf.thenAccept(result -> {
System.out.println("First completed task: " + result);
}).join();
}
}
Code language: Java (java)
4. Handling Exceptions
Exception handling is an essential aspect of asynchronous programming. CompletableFuture
provides methods to handle exceptions gracefully.
Handling Exceptions with exceptionally
You can use the exceptionally
method to provide a fallback value in case of an exception.
import java.util.concurrent.CompletableFuture;
public class CompletableFutureExceptionallyExample {
public static void main(String[] args) {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
if (true) {
throw new RuntimeException("Something went wrong");
}
return "Hello, World!";
});
future.exceptionally(ex -> {
System.err.println("Exception: " + ex.getMessage());
return "Fallback value";
}).thenAccept(System.out::println);
}
}
Code language: Java (java)
Handling Exceptions with handle
The handle
method allows you to process the result and handle the exception in one place.
import java.util.concurrent.CompletableFuture;
public class CompletableFutureHandleExample {
public static void main(String[] args) {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
if (true) {
throw new RuntimeException("Something went wrong");
}
return "Hello, World!";
});
future.handle((result, ex) -> {
if (ex != null) {
System.err.println("Exception: " + ex.getMessage());
return "Fallback value";
}
return result;
}).thenAccept(System.out::println);
}
}
Code language: Java (java)
Combining Exception Handling and Chaining
You can combine exception handling with chaining to create robust asynchronous workflows.
import java.util.concurrent.CompletableFuture;
public class CompletableFutureExceptionHandlingExample {
public static void main(String[] args) {
CompletableFuture.supplyAsync(() -> {
if (true) {
throw new RuntimeException("Something went wrong");
}
return "Hello";
})
.exceptionally(ex -> {
System.err.println("Exception: " + ex.getMessage());
return "Fallback value";
})
.thenApply(result -> result + ", World!")
.thenAccept(System.out::println);
}
}
Code language: Java (java)
5. Using CompletableFuture with Streams
Java Streams and CompletableFuture
can be combined to process collections of asynchronous tasks efficiently.
Transforming a List of Futures
You can transform a list of futures into a future of a list using the allOf
method.
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class CompletableFutureStreamExample {
public static void main(String[] args) {
List<CompletableFuture<String>> futures = IntStream.range(1, 6)
.mapToObj(i -> CompletableFuture.supplyAsync(() -> "Task " + i))
.collect(Collectors.toList());
CompletableFuture<Void> allOf = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
CompletableFuture<List<String>> allFutures = allOf.thenApply(v ->
futures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList())
);
allFutures.thenAccept(results -> {
results.forEach(System.out::println);
}).join();
}
}
Code language: Java (java)
Parallel Processing with Streams
You can use parallel streams to initiate multiple asynchronous tasks in parallel.
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class CompletableFutureParallelStreamExample {
public static void main(String[] args) {
CompletableFuture[] futures = IntStream.range(1, 6)
.parallel()
.mapToObj(i -> CompletableFuture.runAsync(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Task " + i + " completed");
}))
.toArray(CompletableFuture[]::new);
CompletableFuture.allOf(futures).join();
}
}
Code language: Java (java)
6. Real-world Examples
Example 1: Fetching Data from Multiple APIs
Imagine you need to fetch data from multiple APIs and combine the results. CompletableFuture
makes this task straightforward.
import java.util.concurrent.CompletableFuture;
public class FetchDataExample {
public static void main(String[] args) {
CompletableFuture<String> api1 = CompletableFuture.supplyAsync(() -> fetchFromAPI1());
CompletableFuture<String> api2 = CompletableFuture.supplyAsync(() -> fetchFromAPI2());
CompletableFuture<String> api3 = CompletableFuture.supplyAsync(() -> fetchFromAPI3());
CompletableFuture<Void> allOf = CompletableFuture.allOf(api1, api2, api3);
allOf.thenRun(() -> {
String result1 = api1.join();
String result2 = api2.join();
String result3 = api3.join();
String combinedResult = result1 + ", " + result2 + ", " + result3;
System.out.println("Combined Result: " + combinedResult);
}).join();
}
private static String fetchFromAPI1() {
// Simulate API call
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Data from API 1";
}
private static String fetchFromAPI2() {
// Simulate API call
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Data from API 2";
}
private static String fetchFromAPI3() {
// Simulate API call
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Data from API 3";
}
}
Code language: Java (java)
Example 2: Processing a Batch of Tasks
Suppose you need to process a batch of tasks in parallel and collect their results.
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class BatchProcessingExample {
public static void main(String[] args) {
List<CompletableFuture<String>> futures = IntStream.range(1, 11)
.mapToObj(i -> CompletableFuture.supplyAsync(() -> processTask(i)))
.collect(Collectors.toList());
CompletableFuture<Void> allOf = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
CompletableFuture<List<String>> allFutures = allOf.thenApply(v ->
futures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList())
);
allFutures.thenAccept(results -> {
results.forEach(System.out::println);
}).join();
}
private static String processTask(int i) {
// Simulate a long-running task
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Task " + i + " completed";
}
}
Code language: Java (java)
7. Best Practices
Avoid Blocking Calls
Avoid blocking calls like join
or get
inside the CompletableFuture
chain. Instead, use asynchronous methods and callbacks.
Use Executor for Custom Thread Pools
By default, CompletableFuture
uses the ForkJoinPool.commonPool() for async tasks. For better control, provide a custom executor.
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CustomExecutorExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(10);
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
// Task logic here
System.out.println("Task running in custom executor");
}, executor);
future.join();
executor.shutdown();
}
}
Code language: Java (java)
Handle Exceptions Properly
Ensure to handle exceptions at every stage of the CompletableFuture
chain to avoid unexpected behavior.
Use Completion Stages
Completion stages like thenApplyAsync
, thenAcceptAsync
, and thenRunAsync
allow you to offload work to a different thread pool.
Keep Chains Short and Simple
Long chains of CompletableFuture
can become difficult to read and maintain. Break them into smaller, manageable pieces.
8. Conclusion
CompletableFuture
is a versatile and powerful tool for asynchronous programming in Java. It allows you to write non-blocking, efficient, and scalable code by providing a rich API for composing and combining asynchronous tasks. By following best practices and understanding how to handle exceptions and combine futures, you can leverage CompletableFuture
to build robust and responsive applications.
This tutorial has covered the basics of creating, combining, and handling exceptions with CompletableFuture
, along with practical examples and best practices. With this knowledge, you can confidently implement asynchronous programming in your Java projects, improving performance and responsiveness.