Photo by Omar Flores on Unsplash

Java Futures and Kotlin Co-Routines — a comparison

Suchak Jani
DataDrivenInvestor
Published in
9 min readOct 13, 2019

--

Background

Asynchronous programming

Asynchronous programming helps us perform many tasks in parallel. Do read my other posts on the subject:

Java Asynchronous programming

Asynchronous programming in Java has come a long way. Java futures have been constantly improving and are a mainstay of many code bases. Java streams have Java Futures as a base.

Kotlin Asynchronous programming

Kotlin is a new language built by the folks from Idea. Android has accepted it open arms. Here is a link to help us understand “Why ?”

Kotlin has an aspect called coroutines which helps with asynchronous programming.

Examples

In this post, we will code some examples of Java futures and Kotlin coroutines.

Java Futures

Java futures are built around the future interface. They help us look at aspects of asynchronous programming in Java.

We will now code a series of classes.

WorkManager

package future.tryout;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

class WorkManager {

private ExecutorService es = Executors.newCachedThreadPool();

CompletionStage<Result> workForTime(int millis) {
CompletableFuture<Result> future = new CompletableFuture<>();
es.submit(()->{
try {
Thread.sleep(millis);
future.complete(Result.of(millis, true, "Worked for " + millis + " milliseconds"));
} catch (InterruptedException e) {
future.complete(Result.of(millis, false, "Error : " + e.getMessage()));
}
});
return future;
}
}

Workmanager creates a dummy worker which mimics a real work task by sleeping for a given number of milliseconds before a work task is complete.

It then returns a “Result” object. This work is wrapped in a “CompletionStage”. This should help us chain disparate tasks.

Note: As per the Java API, when we sleep, we must also catch or throw an “Interruptedexception”.

Result

package future.tryout;

import lombok.Value;

@Value(staticConstructor = "of")
class Result {
int time;
boolean success;
String message;
}

A simple data class with three fields

  1. int time — the time in milliseconds we waited
  2. boolean success — was the work a success or not
  3. String message — the message from the work task

Using Lombok here to reduce java boilerplate code.

FutureExamples

package future.tryout;

import java.util.Optional;
import java.util.concurrent.CompletionStage;

public class FutureExamples {

private WorkManager manager = new WorkManager();

The main examples class.

We create the workmanager instance and we should be good to go.

Note:

  1. By no means is this an exhaustive list of all the methods we could use with Java Futures. Please see the Java API and work out what you need
  2. These are only examples of a possible way to use these Futures methods.

thenRun

public CompletionStage<Void> workWithRun(int time1, int time2) {
return
manager
.workForTime(time1)
.thenRun(() -> {
manager.workForTime(time2);
});
}

The first example is thenRun.

We have two parameters here, time1 and time2. Once we finish time1 work, we want to run time2.

Notice here thenRun returns a CompletionStage with Void. It does not return our “Result” object. This may be fine for some use cases.

Let’s see if we can return a result in the next method.

thenApply

public CompletionStage<CompletionStage<Result>> workWithApply(int time1, int time2) {
return
manager
.workForTime(time1)
.thenApply(
result -> manager.workForTime(time2) // If we wanted we could do something with result here
);
}

This is a thenApply Example

If we also want to return the result of time2 task, we could use thenApply. As seen above we get the return with is a has the second task result wrapped in two completion stages.

Lets see in the next example if we can improve on this.

thenCompose

public CompletionStage<Result> workWithCompose(int time1, int time2) {
return
manager
.workForTime(time1)
.thenCompose(
result -> manager.workForTime(time2) // If we wanted we could do something with result here
);
}

This is a thenCompose Example

As seen above we get the return with is a has the result in one completion stage.

If you have used the streams api, this is analogous to a flatMap

thenAcceptBoth

public CompletionStage<Void> workWithAccept(int time1, int time2, int time3) {
return
manager
.workForTime(time1)
.thenAcceptBoth(
manager.workForTime(time2), (result, result2) -> {
if (result.isSuccess() && result2.isSuccess()) {
manager.workForTime(time3);
}
});
}

This is an example of thenAcceptBoth with three tasks

Here we execute three tasks, but as before we still just get a void CompletionStage. Lets see if we can improve on that and get a result.

thenCombine

public CompletionStage<Optional<CompletionStage<Result>>> workWithCombine(int time1, int time2, int time3) {
return
manager
.workForTime(time1)
.thenCombine(manager.workForTime(time2),(result, result2) -> {
if (result.isSuccess() && result2.isSuccess()) {
return Optional.of(manager.workForTime(time3));
}
return Optional.empty();
});
}

This is an example of thenCombine with three tasks

Here we execute three tasks and get a Result of the final task.

Exceptions

What if we wanted to know if there was an exception and then we wanted to act on the exception on any CompletionStage ?

We could use “whenComplete” on the result CompletionStage. This is true of all the above examples.

(void, exception) -> {
if (exception == null) {
// triggering stage completed normally
} else {
// triggering stage completed exceptionally
}
}

Kotlin Coroutines

Kotlin coroutines help us with aspects of asynchronous programming in Kotlin. Do go through the guide here

We will now code a series of classes.

WorkManager

package coroutine.tryout

import kotlinx.coroutines.delay

internal class WorkManager {

suspend fun workForTime(millis: Int): Result {
delay(millis.toLong())
return Result(millis, true, "Worked for $millis milliseconds")
}
}

Workmanager creates a dummy worker which mimics a real work task by sleeping for a given number of milliseconds before a work task is complete.

deplay is a special suspending function that does not block a thread, but suspends coroutine and it can be only used from a coroutine.

Result

package coroutine.tryout

data class Result(val time: Int, val isSuccess: Boolean, val message: String)

A simple data class with three fields

  1. int time — the time in milliseconds we waited
  2. boolean success — was the work a success or not
  3. String message — the message from the work task

Love Data classes!

Note: Java may soon have a similar concept i.e. Record classes

CoroutineExamples

package coroutine.tryout

import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope

class CoroutineExamples {

private val manager = WorkManager()

The main examples class.

We create the workmanager instance and we should be good to go.

Note:

  1. By no means is this an exhaustive list of all the functions, we could use with Kotlin CoRoutines. Please see the CoRoutines API and work out what you need
  2. These are only examples of a possible way to use these CoRoutine methods.

Synchronous run with two functions

suspend fun workWithTwoTimes(time1: Int, time2: Int): Pair<Result, Result> {
return coroutineScope {
val result1 = manager.workForTime(time1) // work1
val result2 = manager.workForTime(time2) // work2 after work1
Pair(result1, result2)
}
}

Here we perform two tasks in a coroutine and return the result of both the tasks in a pair class.

Parallel run with two functions

suspend fun workWithTwoTimesParallel(time1: Int, time2: Int): Pair<Result, Result> {
return coroutineScope {
val work1 = async { manager.workForTime(time1) } // Async work1
val result2 = manager.workForTime(time2) // work2 while work1 is working
val result1 = work1.await() // non-blocking wait
Pair(result1, result2)
}
}

Here we perform two tasks, both in parallel and then return the result of both the tasks in a Pair class. Not much more to it. Sweet!

Synchronous run with three functions

suspend fun workWithThreeTimes(time1: Int, time2: Int, time3: Int): Triple<Result, Result, Result> {
return coroutineScope {
val result1 = manager.workForTime(time1) // work1
val result2 = manager.workForTime(time2) // work2 after work1
val result3 = manager.workForTime(time3) // work3 after work2
Triple(result1, result2, result3)
}
}

Here we perform three tasks in a coroutine and return the three results of the tasks in a Triple class.

Parallel run with three functions

suspend fun workWithThreeTimesParallel(time1: Int, time2: Int, time3: Int): Triple<Result, Result, Result> {
return coroutineScope {
val work1 = async { manager.workForTime(time1) } // Async work1
val work2 = async { manager.workForTime(time2) } // Async work2 while work1 is working
val result3 = manager.workForTime(time3) // work3 while work1 and work2 are working
val result1 = work1.await() // non-blocking wait
val result2 = work2.await()// non-blocking wait
Triple(result1, result2, result3)
}
}

Here we perform three works, in parallel, in a coroutine and return the three results of the tasks in a Triple class.

Note:

  1. “async/await” in Java i.e “wait/lock/notify/join” are thread blocking paradigms in java involving thread synchronization.
  2. “async/await” in kotlin is explained as : “Awaits for completion of this value without blocking a thread”
  3. We could use launch in place of async if we do not care about getting the result
  4. Could we do something like this in Java i.e. perform a task and when the task is complete get a notification without blocking a thread ? Yes, we could choose to use any event/message based framework like Vert.x, Akka Actors, Play, etc…

Test

package coroutine.tryout

import io.kotlintest.specs.StringSpec
import kotlin.system.measureTimeMillis


class CoroutineExamplesTimeTest : StringSpec({

val coroutineExamples = CoroutineExamples()
val time1 = 300
val time2 = 200
val time3 = 100

"workWithTwoTimes should return result time1 and result time2" {
val time = measureTimeMillis {
coroutineExamples.workWithTwoTimes(time1, time2)
}
println("workWithTwoTimes in $time ms")
}

"workWithThreeTimes should return result time1, time2 and time3" {
val time = measureTimeMillis {
coroutineExamples.workWithThreeTimes(time1, time2, time3)
}
println("workWithThreeTimes in $time ms")
}

"workWithTwoTimesParallel should return result time1 and result time2" {
val time = measureTimeMillis {
coroutineExamples.workWithTwoTimesParallel(time1, time2)
}
println("workWithTwoTimesParallel in $time ms")
}

"workWithThreeTimesParallel should return result time1, time2 and time3" {
val time = measureTimeMillis {
coroutineExamples.workWithThreeTimesParallel(time1, time2, time3)
}
println("workWithThreeTimesParallel in $time ms")
}

}
)

As seen above we will use measureTimeMillis as a block to test our suspend task functions.

The test above should be self explanatory.

Test output

workWithTwoTimes in 509 ms
workWithThreeTimes in 612 ms
workWithTwoTimesParallel in 304 ms
workWithThreeTimesParallel in 307 ms

We get the result as above.

Running two tasks we use 509ms and running two tasks in parallel we use 304 ms. Makes sense.

Running three tasks we use 612ms and running three tasks in parallel we use 307 ms. Again Makes sense.

Verdict

Kotlin Coroutines make a lot of sense. They seem simple to work with and thus development should be simpler.

What do you think ? Feel free to add any comments/advice/corrections in the comments section below.

Thanks for your time.

Code

All code is checked in here

--

--