1. Introduction

Core Java provides a basic API for asynchronous computations – Future. CompletableFuture is one of its newest implementations.

Vavr provides its new functional alternative to the Future API. In this article, we’ll discuss the new API and show how to make use of some of its new features.

More articles on Vavr can be found here.

2. Maven Dependency

The Future API is included in the Vavr Maven dependency.

So, let’s add it to our pom.xml:

<dependency>
    <groupId>io.vavr</groupId>
    <artifactId>vavr</artifactId>
    <version>0.9.2</version>
</dependency>

We can find the latest version of the dependency on Maven Central.

3. Vavr’s Future

The Future can be in one of two states:

  • Pending – the computation is ongoing
  • Completed – the computation finished successfully with a result, failed with an exception or was canceled

The main advantage over the core Java Future is that we can easily register callbacks and compose operations in a non-blocking way.

4. Basic Future Operations

4.1. Starting Asynchronous Computations

Now, let’s see how we can start asynchronous computations using Vavr:

String initialValue = "Welcome to ";
Future<String> resultFuture = Future.of(() -> someComputation());

4.2. Retrieving Values from a Future

We can extract values from a Future by simply calling one of the get() or getOrElse() methods:

String result = resultFuture.getOrElse("Failed to get underlying value.");

The difference between get() and getOrElse() is that get() is the simplest solution, while getOrElse() enables us to return a value of any type in case we weren’t able to retrieve the value inside the Future.

It’s recommended to use getOrElse() so we can handle any errors that occur while trying to retrieve the value from a Future. For the sake of simplicity, we’ll just use get() in the next few examples.

Note that the get() method blocks the current thread if it’s necessary to wait for the result.

A different approach is to call the nonblocking getValue() method, which returns an Option<Try> which will be empty as long as computation is pending.

We can then extract the computation result which is inside the Try object:

Option<Try<String>> futureOption = resultFuture.getValue();
Try<String> futureTry = futureOption.get();
String result = futureTry.get();

Sometimes we need to check if the Future contains a value before retrieving values from it.

We can simply do that by using:

resultFuture.isEmpty();

It’s important to note that the method isEmpty() is blocking – it will block the thread until its operation is finished.

4.3. Changing the Default ExecutorService

Futures use an ExecutorService to run their computations asynchronously. The default ExecutorService is Executors.newCachedThreadPool().

We can use another ExecutorService by passing an implementation of our choice:

@Test
public void whenChangeExecutorService_thenCorrect() {
    String result = Future.of(newSingleThreadExecutor(), () -> HELLO)
      .getOrElse(error);
    
    assertThat(result)
      .isEqualTo(HELLO);
}

5. Performing Actions Upon Completion

The API provides the onSuccess() method which performs an action as soon as the Future completes successfully.

Similarly, the method onFailure() is executed upon the failure of the Future.

Let’s see a quick example:

Future<String> resultFuture = Future.of(() -> appendData(initialValue))
  .onSuccess(v -> System.out.println("Successfully Completed - Result: " + v))
  .onFailure(v -> System.out.println("Failed - Result: " + v));

The method onComplete() accepts an action to be run as soon as the Future has completed its execution, whether or not the Future was successful. The method andThen() is similar to onComplete() – it just guarantees the callbacks are executed in a specific order:

Future<String> resultFuture = Future.of(() -> appendData(initialValue))
  .andThen(finalResult -> System.out.println("Completed - 1: " + finalResult))
  .andThen(finalResult -> System.out.println("Completed - 2: " + finalResult));

6. Useful Operations on Futures

6.1. Blocking the Current Thread

The method await() has two cases:

  • if the Future is pending, it blocks the current thread until the Future has completed
  • if the Future is completed, it finishes immediately

Using this method is straightforward:

resultFuture.await();

6.2. Canceling a Computation

We can always cancel the computation:

resultFuture.cancel();

6.3. Retrieving the Underlying ExecutorService

To obtain the ExecutorService that is used by a Future, we can simply call executorService():

resultFuture.executorService();

6.4. Obtaining a Throwable from a Failed Future

We can do that using the getCause() method which returns the Throwable wrapped in an io.vavr.control.Option object.

We can later extract the Throwable from the Option object:

@Test
public void whenDivideByZero_thenGetThrowable2() {
    Future<Integer> resultFuture = Future.of(() -> 10 / 0)
      .await();
    
    assertThat(resultFuture.getCause().get().getMessage())
      .isEqualTo("/ by zero");
}

Additionally, we can convert our instance to a Future holding a Throwable instance using the failed() method:

@Test
public void whenDivideByZero_thenGetThrowable1() {
    Future<Integer> resultFuture = Future.of(() -> 10 / 0);
    
    assertThatThrownBy(resultFuture::get)
      .isInstanceOf(ArithmeticException.class);
}

6.5. isCompleted(), isSuccess(), and isFailure()

These methods are pretty much self-explanatory. They check if a Future completed, whether it completed successfully or with a failure. All of them return boolean values, of course.

We’re going to use these methods with the previous example:

@Test
public void whenDivideByZero_thenCorrect() {
    Future<Integer> resultFuture = Future.of(() -> 10 / 0)
      .await();
    
    assertThat(resultFuture.isCompleted()).isTrue();
    assertThat(resultFuture.isSuccess()).isFalse();
    assertThat(resultFuture.isFailure()).isTrue();
}

6.6. Applying Computations on Top of a Future

The map() method allows us to apply a computation on top of a pending Future:

@Test
public void whenCallMap_thenCorrect() {
    Future<String> futureResult = Future.of(() -> "from Baeldung")
      .map(a -> "Hello " + a)
      .await();
    
    assertThat(futureResult.get())
      .isEqualTo("Hello from Baeldung");
}

If we pass a function that returns a Future to the map() method, we can end up with a nested Future structure. To avoid this, we can leverage the flatMap() method:

@Test
public void whenCallFlatMap_thenCorrect() {
    Future<Object> futureMap = Future.of(() -> 1)
      .flatMap((i) -> Future.of(() -> "Hello: " + i));
         
    assertThat(futureMap.get()).isEqualTo("Hello: 1");
}

6.7. Transforming Futures

The method transformValue() can be used to apply a computation on top of a Future and change the value inside it to another value of the same type or a different type:

@Test
public void whenTransform_thenCorrect() {
    Future<Object> future = Future.of(() -> 5)
      .transformValue(result -> Try.of(() -> HELLO + result.get()));
                
    assertThat(future.get()).isEqualTo(HELLO + 5);
}

6.8. Zipping Futures

The API provides the zip() method which zips Futures together into tuples – a tuple is a collection of several elements that may or may not be related to each other. They can also be of different types. Let’s see a quick example:

@Test
public void whenCallZip_thenCorrect() {
    Future<String> f1 = Future.of(() -> "hello1");
    Future<String> f2 = Future.of(() -> "hello2");
    
    assertThat(f1.zip(f2).get())
      .isEqualTo(Tuple.of("hello1", "hello2"));
}

The point to note here is that the resulting Future will be pending as long as at least one of the base Futures is still pending.

6.9. Conversion Between Futures and CompletableFutures

The API supports integration with java.util.CompletableFuture. So, we can easily convert a Future to a CompletableFuture if we want to perform operations that only the core Java API supports.

Let’s see how we can do that:

@Test
public void whenConvertToCompletableFuture_thenCorrect()
  throws Exception {
 
    CompletableFuture<String> convertedFuture = Future.of(() -> HELLO)
      .toCompletableFuture();
    
    assertThat(convertedFuture.get())
      .isEqualTo(HELLO);
}

We can also convert a CompletableFuture to a Future using the fromCompletableFuture() method.

6.10. Exception Handling

Upon the failure of a Future, we can handle the error in a few ways.

For example, we can make use of the method recover() to return another result, such as an error message:

@Test
public void whenFutureFails_thenGetErrorMessage() {
    Future<String> future = Future.of(() -> "Hello".substring(-1))
      .recover(x -> "fallback value");
    
    assertThat(future.get())
      .isEqualTo("fallback value");
}

Or, we can return the result of another Future computation using recoverWith():

@Test
public void whenFutureFails_thenGetAnotherFuture() {
    Future<String> future = Future.of(() -> "Hello".substring(-1))
      .recoverWith(x -> Future.of(() -> "fallback value"));
    
    assertThat(future.get())
      .isEqualTo("fallback value");
}

The method fallbackTo() is another way to handle errors. It’s called on a Future and accepts another Future as a parameter.

If the first Future is successful, then it returns its result. Otherwise, if the second Future is successful, then it returns its result. If both Futures fail, then the failed() method returns a Future of a Throwable, which holds the error of the first Future:

@Test
public void whenBothFuturesFail_thenGetErrorMessage() {
    Future<String> f1 = Future.of(() -> "Hello".substring(-1));
    Future<String> f2 = Future.of(() -> "Hello".substring(-2));
    
    Future<String> errorMessageFuture = f1.fallbackTo(f2);
    Future<Throwable> errorMessage = errorMessageFuture.failed();
    
    assertThat(
      errorMessage.get().getMessage())
      .isEqualTo("begin -1, end 5, length 5");
}

7. Conclusion

In this article, we’ve seen what a Future is and learned some of its important concepts. We’ve also walked through some of the features of the API using a few practical examples.

The full version of the code is available over on GitHub.


» 下一篇: Java中的弱引用