CompletableFuture
class as a powerful tool for asynchronous programming. CompletableFuture
provides a rich set of APIs that allow developers to write asynchronous, non - blocking code easily, enabling applications to handle multiple tasks concurrently without waiting for each task to complete sequentially. This blog will explore the fundamental concepts, usage methods, common practices, and best practices of CompletableFuture
in Java.A CompletableFuture
in Java represents a future result of an asynchronous computation. It can be thought of as a container that will eventually hold a value when the computation is completed. The key idea behind CompletableFuture
is that it allows you to perform operations on the result once it becomes available, without blocking the current thread.
Asynchronous operations are executed in a separate thread, typically from a thread pool. This enables the main thread to continue executing other tasks while waiting for the asynchronous operation to finish. CompletableFuture
also supports chaining of operations, which means you can define a sequence of actions to be performed on the result as it becomes available.
There are several ways to create a CompletableFuture
:
CompletableFuture.runAsync()
This method is used when you have a task that does not return a result. It takes a Runnable
as an argument.
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CompletableFutureExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newSingleThreadExecutor();
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
try {
Thread.sleep(2000);
System.out.println("Task completed");
} catch (InterruptedException e) {
e.printStackTrace();
}
}, executor);
executor.shutdown();
}
}
CompletableFuture.supplyAsync()
This method is used when your task returns a result. It takes a Supplier
as an argument.
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CompletableFutureSupplyExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newSingleThreadExecutor();
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(2000);
return "Result from asynchronous task";
} catch (InterruptedException e) {
e.printStackTrace();
return null;
}
}, executor);
executor.shutdown();
}
}
You can chain operations on a CompletableFuture
using methods like thenApply()
, thenAccept()
, and thenRun()
.
thenApply()
This method takes a Function
and applies it to the result of the CompletableFuture
when it becomes available.
import java.util.concurrent.CompletableFuture;
public class ThenApplyExample {
public static void main(String[] args) {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Hello")
.thenApply(s -> s + " World");
future.thenAccept(System.out::println);
}
}
thenAccept()
This method takes a Consumer
and consumes the result of the CompletableFuture
when it becomes available.
import java.util.concurrent.CompletableFuture;
public class ThenAcceptExample {
public static void main(String[] args) {
CompletableFuture.supplyAsync(() -> "Hello")
.thenAccept(s -> System.out.println("Received: " + s));
}
}
thenRun()
This method takes a Runnable
and runs it when the CompletableFuture
is completed.
import java.util.concurrent.CompletableFuture;
public class ThenRunExample {
public static void main(String[] args) {
CompletableFuture.supplyAsync(() -> "Hello")
.thenRun(() -> System.out.println("Task finished"));
}
}
You can combine multiple CompletableFutures
using methods like thenCombine()
and thenCompose()
.
thenCombine()
This method combines the results of two CompletableFutures
using a BiFunction
.
import java.util.concurrent.CompletableFuture;
public class ThenCombineExample {
public static void main(String[] args) {
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Hello");
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> " World");
CompletableFuture<String> combinedFuture = future1.thenCombine(future2, (s1, s2) -> s1 + s2);
combinedFuture.thenAccept(System.out::println);
}
}
thenCompose()
This method is used when you have a CompletableFuture
that returns another CompletableFuture
.
import java.util.concurrent.CompletableFuture;
public class ThenComposeExample {
public static void main(String[] args) {
CompletableFuture<CompletableFuture<String>> nestedFuture = CompletableFuture.supplyAsync(() -> {
return CompletableFuture.supplyAsync(() -> "Hello World");
});
CompletableFuture<String> flatFuture = nestedFuture.thenCompose(f -> f);
flatFuture.thenAccept(System.out::println);
}
}
You can use CompletableFuture
to execute multiple tasks in parallel and wait for all of them to complete.
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ParallelTaskExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(2);
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(2000);
return "Result 1";
} catch (InterruptedException e) {
e.printStackTrace();
return null;
}
}, executor);
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(3000);
return "Result 2";
} catch (InterruptedException e) {
e.printStackTrace();
return null;
}
}, executor);
CompletableFuture<Void> allFutures = CompletableFuture.allOf(future1, future2);
CompletableFuture<String> combinedResult = allFutures.thenApply(v -> {
return future1.join() + " " + future2.join();
});
combinedResult.thenAccept(System.out::println);
executor.shutdown();
}
}
You can handle errors in a CompletableFuture
using the exceptionally()
method.
import java.util.concurrent.CompletableFuture;
public class ErrorHandlingExample {
public static void main(String[] args) {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
throw new RuntimeException("Error occurred");
}).exceptionally(ex -> {
System.out.println("Caught exception: " + ex.getMessage());
return "Default value";
});
future.thenAccept(System.out::println);
}
}
CompletableFuture
, it is recommended to use custom thread pools instead of relying on the common fork - join pool. This gives you more control over the number of threads and their behavior.CompletableFuture
chains to prevent unexpected failures. Use methods like exceptionally()
, handle()
, and whenComplete()
for error handling.CompletableFutures
as it can make the code hard to read and maintain. Use thenCompose()
to flatten nested futures.Java’s CompletableFuture
is a powerful and flexible tool for asynchronous programming. It provides a wide range of APIs that allow developers to write complex asynchronous code easily. By understanding the fundamental concepts, usage methods, common practices, and best practices, you can leverage the full potential of CompletableFuture
to improve the performance and responsiveness of your Java applications.