Completable Futures

Completable Futures #

CompletableFutures extend futures with numerous methods to create more complex futures from simpler ones. We will discuss selected parts of their API using tests.

Creating futures #

Completed futures can be created from the value they return.

void testCreatingCompletedFuture() {
    final CompletableFuture<Integer> future = //
	CompletableFuture.completedFuture(42);
    assertEquals(42, future.join());
}

Here, the join method is like get but does not throw an InterruptedException. We can use supplyAsync to create a future from an asynchronous computation.

void testCompletingFutureAsynchronously() {
    final CompletableFuture<Integer> future = //
        CompletableFuture.supplyAsync(() -> 42);
    assertEquals(42, future.join());
} 

The computation is executed on the common ForkJoinPool by default. An alternative executor can be passed as second argument. An even more manual way to create a completed future is to call complete from an arbitrary thread.

void testCompletingFutureManually() {
    final CompletableFuture<Integer> future = new CompletableFuture<>();
    Executors.newSingleThreadExecutor().execute(() -> future.complete(42));
    assertEquals(42, future.join());
}

Manipulating futures #

The interface CompletionStage implemented by completable futures defines many operations for manipulating completable futures. We only show two methods here, one that is like map on streams and another that is like flatMap.

void testThatThenApplyIsLikeMap() {
    final CompletableFuture<String> stringFuture = //
        CompletableFuture.completedFuture("hello");
    final CompletableFuture<Integer> intFuture = //
        stringFuture.thenApply(String::length);
    assertEquals(5, intFuture.join());
}

void testThatThenComposeIsLikeFlatMap() {
    final CompletableFuture<String> stringFuture = //
        CompletableFuture.completedFuture("ha");
    final CompletableFuture<String> combinedFuture = //
        stringFuture.thenCompose(string -> //
        CompletableFuture.completedFuture(string+string));
    assertEquals("haha", combinedFuture.join());
}

The second test could also be implemented using thenApply rather than thenCompose. We can see that just like map is a special case of flatMap for streams (and optionals, too) thenApply is a special case of thenCompose for completable futures.

Task: Handling exceptions #

Write more tests for operations involving completable futures that you find interesting. Include tests that show operations involving exceptions and how to handle them. Can you express something like filter for streams using thenCompose and exceptional futures?