- Published on
Java CompletableFuture vs Kotlin Coroutines
- Authors
- Name
- Luis Carbonel
Introduction
Asynchronous programming offers developers a variety of options, including Kotlin coroutines and Java CompletableFuture. Kotlin's coroutines provide a lightweight and efficient approach to concurrent programming, while Java CompletableFuture offers a traditional way to manage asynchronous tasks. In this post, we will see what a Java CompletableFuture is. We will illustrate it with some examples, and later we will try to implement those examples with Kotlin using coroutines.
What is a CompletableFuture?
CompletableFuture is a class in Java that represents the result of an asynchronous operation. It’s part of the Java Standard Library and is available in Java 8 and later versions.
A CompletableFuture can be used to represent a computation that may not be completed yet. You can start the computation and continue to do other things while the computation is running in the background. Once the computation is complete, you can retrieve its result using the CompletableFuture.
CompletableFuture provides a variety of methods for composing, transforming, and combining the results of multiple asynchronous operations. For example, you can use the thenApply method to transform the result of a CompletableFuture, the thenCombine method to combine the results of two CompletableFuture objects, and the thenAccept method to perform an action with the result of a CompletableFuture.
CompletableFuture also provides a variety of methods for handling errors and exceptions that might occur during the computation. For example, you can use the exceptionally method to handle exceptions, or the handle method to handle both exceptions and successful results.
Here’s an example implementation of CompletableFuture in Java:
import java.util.concurrent.*;
public class CompletableFutureExample {
public static void main(String[] args) {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Hello World";
});
future.thenAccept(System.out::println)
.thenAccept(v -> System.out.println("Computation done!"))
.join();
}
}
In this example, we use the CompletableFuture.supplyAsync method to create a new CompletableFuture that runs asynchronously. The supplyAsync method takes a Supplier as an argument, which is a function that returns a value. In this case, the Supplier simply returns the string “Hello World” after sleeping for 2 seconds.
We then use the thenAccept method to specify a function that should be executed when the CompletableFuture is complete. In this case, we pass a method reference to System.out.println, which will print the result of the CompletableFuture to the console.
Finally, we call the join method to wait for the CompletableFuture to complete. The join method blocks the current thread until the CompletableFuture is complete, at which point the result will be printed to the console.
You can combine two CompletableFuture objects by using the thenCombine method, which takes two CompletableFuture objects and a BiFunction as arguments. The BiFunction is a function that takes two arguments and returns a value, which is used to combine the results of the two CompletableFuture objects.
import java.util.concurrent.*;
public class CompletableFutureCombineExample {
public static void main(String[] args) {
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Hello";
});
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "World";
});
future1.thenCombine(future2, (s1, s2) -> s1 + " " + s2)
.thenAccept(System.out::println)
.join();
}
}
In this example, we have two CompletableFuture objects, future1 and future2, each of which returns a string. We use the thenCombine method to combine the results of the two CompletableFuture objects into a single string. The BiFunction passed to thenCombine takes two strings as arguments and returns a concatenated string.
Finally, we use the thenAccept method to print the combined string to the console. The join method is called to wait for the CompletableFuture to complete and ensure that the result is printed to the console before the program terminates.
Now we are going to solve the previous example but using Kotlin coroutines. Wait wait, first of all, let’s define what Kotlin coroutines are.
What are Kotlin Coroutines?
Kotlin's coroutines are a way to write asynchronous code. They are similar to Java threads, but they are much lighter weight and more efficient. Coroutines are a way to write asynchronous code that is easier to read and write than using callbacks. They are also more efficient than using threads because they don’t require the overhead of creating and managing threads. If you are interested in this topic, I invite you to read Kotlin coroutines a basic approach that explains it in a more detailed way.
Here’s an example implementation of combining two asynchronous operations using Kotlin coroutines:
import kotlinx.coroutines.*
fun main() {
runBlocking {
val job1 = async {
delay(1000)
"Hello"
}
val job2 = async {
delay(2000)
"World"
}
println("${job1.await()} ${job2.await()}")
}
}
In this example, we use the async function to create two Deferred objects, which represent asynchronous operations that can be combined using the await function. The async function takes a suspending block as an argument, which is executed asynchronously.
The runBlocking function is used to create a new coroutine that runs on the current thread and blocks until it’s complete. Within the runBlocking block, we use the await function to wait for the Deferred objects to complete and retrieve their results. The results are then combined into a single string and printed to the console.
Note that, unlike CompletableFuture, coroutines do not throw checked exceptions, so you don’t have to handle exceptions such as InterruptedException. Additionally, coroutines provide a more concise and readable syntax compared to CompletableFuture, making it easier to write and maintain asynchronous code.
Is it possible to use CompletableFuture in Kotlin?
Of course, it is possible to use CompletableFutures in Kotlin since Kotlin is designed to be compatible with Java libraries. Here is an example that you will be familiar with as we have seen it implemented with Java.
// combining two asynchronous operations using CompletableFuture in Kotlin
fun main() {
val future1 = CompletableFuture.supplyAsync {
Thread.sleep(1000)
"Hello"
}
val future2 =CompletableFuture.supplyAsync {
Thread.sleep(2000)
"World"
}
future1.thenCombine(future2) { s1, s2 -> "$s1 $s2" }
.thenAccept { println(it) }
.join()
}
// Output: Hello World
But will it be possible to run CompletableFuture on a Kotlin coroutine?
I’m sure you’re imagining the answer. Of course we can do it, let’s continue playing with the previous example and let’s combine two asynchronous operations using CompletableFuture and Kotlin coroutines.
fun main() = runBlocking {
val future1 = async {
CompletableFuture.supplyAsync {
Thread.sleep(1000)
"Hello"
}
}
val future2 = async {
CompletableFuture.supplyAsync {
Thread.sleep(2000)
"World"
}
}
future1.await().thenCombine(future2.await()) { s1, s2 -> "$s1 $s2" }
.thenAccept { println(it) }
.join()
}
// Output: Hello World
Kotlin’s coroutines to Java CompletableFuture?
We have seen how to use CompletableFuture in Kotlin and how to use Kotlin coroutines to run CompletableFuture. But what if we want to use Kotlin coroutines to run CompletableFuture? Is it possible? The answer is yes, it is possible. Let’s see an example.
fun main(): Unit = runBlocking {
measureTimeMillis {
val worldJob = async {
delay(3000)
"World"
}
val helloCompletableFuture = helloCompletableFuture()
// if we combine helloCompletableFuture with worldJob, we get a compilation error because
// worldJob is a Job and not a CompletableFuture
/*
helloCompletableFuture.thenCombine(worldJob.await()) { s1, s2 -> "$s1 $s2" }
.thenAccept { println(it) }
.join()
*/
// convert worldJob to CompletableFuture
val worldCompletableFuture = worldJob.asCompletableFuture()
// now we can combine helloCompletableFuture with worldCompletableFuture
helloCompletableFuture.thenCombine(worldCompletableFuture) { s1, s2 -> "$s1 $s2" }
.thenAccept { println(it) }
.join()
}.also { println("Time: $it") }
// Output: Hello World
// Time: 3001
}
fun helloCompletableFuture(): CompletableFuture<String> {
return CompletableFuture.supplyAsync {
Thread.sleep(1000)
"Hello"
}
}
// Kotlin coroutines to CompletableFuture
suspend fun <T> Deferred<T>.asCompletableFuture(): CompletableFuture<T> {
return CompletableFuture.completedFuture(await())
}
This Kotlin code demonstrates the conversion of a Kotlin coroutine Deferred object to a Java CompletableFuture.
First, we define a main function that uses runBlocking to start a new coroutine. Inside the runBlocking block, we call the measureTimeMillis function to measure the time it takes to execute the code inside the block.
We then create a new worldJob coroutine using async. This coroutine will delay for 3 seconds and then return the string “World”.
Next, we call the helloComplasCompletableFuture function to create a new CompletableFuture object that will delay for 1 second and then return the string “Hello”.
We then convert the worldJob coroutine to a CompletableFuture object using the asCompletableFuture extension function that we defined later in the code.
Finally, we combine the two CompletableFuture objects using the thenCombine function to concatenate the strings “Hello” and “World”. We then print the result to the console using thenAccept and wait for the operation to complete using join.
The helloCompletableFuture function creates a new CompletableFuture object that delays for 1 second and then returns the string “Hello”.
The asCompletableFuture extension function is defined to convert a Deferred object to a CompletableFuture object. It uses the await function to get the result of the coroutine and then wraps it in a completed CompletableFuture.
When the code is executed, it will output “Hello World” and the time it took to execute the code.
Differences between CompletableFuture and coroutines
CompletableFuture and coroutines are both ways to write asynchronous code, but they have some important differences.
- CompletableFuture is part of the Java language, while coroutines are a Kotlin-specific feature.
- CompletableFuture uses chained methods, such as thenApply, thenCompose, etc…, to perform tasks, while coroutines use keywords such as suspend, launch, and async to handle concurrency and sequential.
- Coroutines are more flexible and can be used to handle a wide variety of asynchronous operations, while CompletableFuture is specifically designed to work with asynchronous tasks and future results.
Conclusion
In this article, we have seen how to combine the results of two asynchronous operations using CompletableFuture and Kotlin coroutines. We have also seen how Kotlin coroutines are a more concise and readable way to write asynchronous code compared to CompletableFuture. The above does not overshadow the fact that CompletableFuture is a powerful and flexible tool for writing asynchronous code in Java, and can be used to write efficient and scalable applications. However, if you are writing new code in Kotlin, I recommend using Kotlin coroutines instead of CompletableFuture, as they are easier to read and write, and are more efficient than threads. All the code snippets mentioned in the article can be found over on GitHub.