Skip to content

Instantly share code, notes, and snippets.

@A248
Last active March 12, 2023 22:10
Show Gist options
  • Save A248/72f1363284510aec3fd757b93b9fc67d to your computer and use it in GitHub Desktop.
Save A248/72f1363284510aec3fd757b93b9fc67d to your computer and use it in GitHub Desktop.

Using CompletableFuture

CompletableFuture is a plain Java class. It represents the result of an asynchronous computation. A future object is something which will become attainable in the future. When the asynchronous computation finishes, the future is completed.

Introduced in Java 8, CompletableFuture implements the older Future and adds support for callbacks, mapping operations, and cleaner awaiting of completion. If you are using Java 9 or later, you can also use delays and timeouts, subclass CompletableFuture to change the default executor, and take advantage of more utility methods.

Callbacks and operations

What are callbacks?

Using callbacks allows you to run a task when a future completes. The simplest callbacks are thenRun and thenAccept, the latter of which allows you to use the result of the future in your callback.

This is a callback:

CompletableFuture<MyResult> future = // ...
future.thenAccept((result) -> {
  // Here, you have a MyResult object
  // you can call methods on MyResult as you normally would
  result.sayHi();

});

Operations

Other CompletableFuture methods allow you to transform a future of one object into a future of another. The most simple is thenApply. When the first future is completed, its result is fed to the mapping function. Once that mapping function returns, the second future is completed with its result.

What happens if a method returns CompletableFuture?

In that case, you need to accept that you have a CompletableFuture and not the Object itself. You should continue your logic inside a callback, using the thenAccept method. Let's use the example of CompletableFuture<Foo>:

CompletableFuture<Foo> futureFoo = // ...
futureFoo.thenAccept((foo) -> {
  // now you have the Foo
  System.out.println("The foo is " + foo);
  System.out.println("This message appears after futureFoo is completed");
});

When the future is completed, all callbacks are triggered. Usually callbacks use lambdas, but this is no requirement.

If you don't care about the value of the CompletableFuture when it completes, you can use the thenRun method.

How do I return a value that relies on a CompletableFuture?

This is a little bit more complicated. Sometimes you need to return a result which involves the Foo, suppose we call it Bar. You have written a partially complete method, but you don't now how to return Bar.

public Bar getBar() {
  CompletableFuture<Foo> futureFoo = // ...

  // How do I get "theFoo"?
  return new Bar(theFoo);
}

The Lazy Way

If you absolutely must return a Bar immediately, there is nothing you can do except block the current thread and wait for the future to complete. You do this with CompletableFuture.join().

public Bar getBar() {
  CompletableFuture<Foo> futureFoo = // ...

  Foo theFoo = futureFoo.join();
  return new Bar(theFoo);
}

However, this approach has its drawbacks. You do not know how long futureFoo may take to complete. Maybe it's a database request which takes seconds to finish because some user's server has a MySQL database on the other side of the world. Using join will block the current thread until the future is complete.

The Comprehensive Way

The other way is to change the return type of your method to CompletableFuture<Bar>, and use one of the mapping operations:

public CompletableFuture<Bar> getBar() {
  CompletableFuture<Foo> futureFoo = // ...

  CompletableFuture<Bar> result = futureFoo.thenApply((theFoo) -> {
    return new Bar(theFoo);
  });
  return result;
}

Now, you have transformed a future Foo into a future Bar using thenApply. However, this approach may require some refactoring. You might need to change the return types of other methods relying on this one.

Types of CompletableFuture methods

You will notice three variants of all methods made for callback and mapping operations. For example:

thenAccept(Consumer<T>)
thenAcceptAsync(Consumer<T>)
thenAcceptAsync(Consumer<T>, Executor)

These methods are actually best explained in reverse direction

The third variant

The third variant of the methods accepts the operation you want to run, and an Executor in which to run it. The Executor determines on which thread the operation is run. You can supply the thread pool of your choice. For example,

Executor executor;
ExecutorService executorService = Executors.newFixedThreadPool(5); // Creates a thread pool of 5 in size
// Who says CompletableFuture needs to be used for everything? You can execute Runnables in the Executor directly
executor.execute(() -> {
  // do something here
});
CompletableFuture<FirstResult> firstFuture = CompletableFuture.supplyAsync(() -> {
  return new FirstResult();
}, executor);
CompletableFuture<SecondResult> secondFuture = futureFromPool.thenApplyAsync((firstResult) -> {
  return new SecondResult(firstResult);
}, executor);
executorService.shutdown();

Remember to shutdown any ExecutorServices you create when you are done using them. In this example the ExecutorService has a local scope, but for most use cases you will store one in a field.

The second variant

The second variant is similar to the third variant, however, it chooses the ExecutorService for you. This way, you don't have to worry too much about shutting down an ExecutorService.

Internally, the second variant uses ForkJoinPool.commonPool() as the Executor (You can change the default executor in Java 9).

But this is not free - you shouldn't use this variant for all methods. If your code is blocking (file IO, network operations), you should use your own ExecutorService. The common pool is intended only for computational work.

The first variant

The first variant of method is intended for small and lightweight operations. It can run in any thread, including the calling thread or the thread completing the future. You should not rely on the thread in which your operation runs, therefore it is suitable when you have no care for which thread the operation runs in.

Summary

You may have noticed the symmetry of much of this tutorial:

  • What are callbacks?
    • Operations which run when a future completes
  • What are mapping operations?
    • Operations which run when a future completes, which map that future to a new kind of future
  • How do I use a CompletableFuture returned from another method?
    • The answer is to use a callback
  • How do I return a value using a CompletableFuture?
    • The answer is to use a mapping operation
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment