Перейти к основному содержимому

Введение в будущее в Вавре

· 7 мин. чтения

1. Введение

Core Java предоставляет базовый API для асинхронных вычислений — будущее. CompletableFuture — одна из новейших его реализаций.

Vavr предоставляет новую функциональную альтернативу Future API. В этой статье мы обсудим новый API и покажем, как использовать некоторые из его новых функций.

Больше статей о Vavr можно найти здесь .

2. Зависимость от Maven

Future API включен в зависимость Vavr Maven.

Итак, давайте добавим его в наш pom.xml :

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

Мы можем найти последнюю версию зависимости на Maven Central .

3. Будущее Вавра

Будущее может находиться в одном из двух состояний :

  • Pending – расчет продолжается
  • Завершено — вычисление завершилось успешно с результатом, завершилось ошибкой с исключением или было отменено.

Основное преимущество по сравнению с ядром Java Future заключается в том, что мы можем легко регистрировать обратные вызовы и составлять операции неблокирующим образом.

4. Основные будущие операции

4.1. Запуск асинхронных вычислений

Теперь давайте посмотрим, как мы можем запустить асинхронные вычисления с помощью Vavr:

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

4.2. Получение значений из будущего

Мы можем извлечь значения из Future , просто вызвав один из методов get() или getOrElse() :

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

Разница между get() и getOrElse() заключается в том, что get() является самым простым решением, тогда как getOrElse() позволяет нам вернуть значение любого типа, если мы не смогли получить значение внутри Future .

Рекомендуется использовать getOrElse() , чтобы мы могли обрабатывать любые ошибки, возникающие при попытке получить значение из Future . Для простоты мы будем использовать только get() в следующих нескольких примерах.

Обратите внимание, что метод get() блокирует текущий поток, если необходимо дождаться результата.

Другой подход заключается в вызове неблокирующего метода getValue() , который возвращает Option<Try<T>> , который будет пустым, пока выполняется вычисление.

Затем мы можем извлечь результат вычисления, который находится внутри объекта Try :

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

Иногда нам нужно проверить, содержит ли Future значение, прежде чем извлекать из него значения.

Мы можем просто сделать это, используя:

resultFuture.isEmpty();

Важно отметить, что метод isEmpty() является блокирующим — он будет блокировать поток до тех пор, пока его работа не будет завершена.

4.3. Изменение службы ExecutorService по умолчанию

Фьючерсы используют ExecutorService для асинхронного выполнения своих вычислений. ExecutorService по умолчанию — Executors.newCachedThreadPool() .

Мы можем использовать другой ExecutorService , передав реализацию по нашему выбору:

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

assertThat(result)
.isEqualTo(HELLO);
}

5. Выполнение действий по завершении

API предоставляет метод onSuccess() , который выполняет действие, как только Future успешно завершится.

Точно так же метод onFailure() выполняется при сбое Future .

Давайте посмотрим на быстрый пример:

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

Метод onComplete() принимает действие, которое будет запущено, как только Future завершит свое выполнение, независимо от того, было ли Future успешным. Метод andThen() похож на onComplete() — он просто гарантирует, что обратные вызовы будут выполняться в определенном порядке:

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

6. Полезные операции с фьючерсами

6.1. Блокировка текущего потока

Метод await() имеет два случая:

  • если Future ожидается, он блокирует текущий поток до тех пор, пока Future не завершится
  • если Будущее завершено, оно заканчивается немедленно

Использовать этот метод просто:

resultFuture.await();

6.2. Отмена вычисления

Мы всегда можем отменить вычисление:

resultFuture.cancel();

6.3. Получение базового ExecutorService

Чтобы получить ExecutorService , который используется Future , мы можем просто вызвать executorService() :

resultFuture.executorService();

6.4. Получение Throwable из неудачного Future

Мы можем сделать это с помощью метода getCause() , который возвращает Throwable , заключенный в объект io.vavr.control.Option .

Позже мы можем извлечь Throwable из объекта Option :

@Test
public void whenDivideByZero_thenGetThrowable2() {
Future<Integer> resultFuture = Future.of(() -> 10 / 0)
.await();

assertThat(resultFuture.getCause().get().getMessage())
.isEqualTo("/ by zero");
}

Кроме того, мы можем преобразовать наш экземпляр в Future , содержащий экземпляр Throwable , используя метод failed() :

@Test
public void whenDivideByZero_thenGetThrowable1() {
Future<Integer> resultFuture = Future.of(() -> 10 / 0);

assertThatThrownBy(resultFuture::get)
.isInstanceOf(ArithmeticException.class);
}

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

Эти методы в значительной степени говорят сами за себя. Они проверяют, завершено ли Future , успешно или неудачно. Разумеется, все они возвращают логические значения.

Мы собираемся использовать эти методы в предыдущем примере:

@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. Применение вычислений на вершине будущего

Метод map() позволяет нам применить вычисление поверх ожидающего Future:

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

assertThat(futureResult.get())
.isEqualTo("Hello from ForEach");
}

Если мы передаем функцию, которая возвращает Future методу map() , мы можем получить вложенную структуру Future . Чтобы избежать этого, мы можем использовать метод flatMap() :

@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. Преобразование будущего

Метод transformValue() можно использовать для применения вычислений поверх Future и изменения значения внутри него на другое значение того же типа или другого типа:

@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. Архивация фьючерсов

API предоставляет метод zip() , который объединяет фьючерсы в кортежи — кортеж — это набор нескольких элементов, которые могут быть связаны или не связаны друг с другом. Они тоже могут быть разных видов. Давайте посмотрим на быстрый пример:

@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"));
}

Здесь следует отметить, что результирующий фьючерс будет отложен до тех пор, пока хотя бы один из базовых фьючерсов все еще находится на рассмотрении.

6.9. Преобразование между Futures и CompletableFutures

API поддерживает интеграцию с java.util.CompletableFuture . Таким образом, мы можем легко преобразовать Future в CompletableFuture , если хотим выполнять операции, которые поддерживает только основной Java API.

Давайте посмотрим, как мы можем это сделать:

@Test
public void whenConvertToCompletableFuture_thenCorrect()
throws Exception {

CompletableFuture<String> convertedFuture = Future.of(() -> HELLO)
.toCompletableFuture();

assertThat(convertedFuture.get())
.isEqualTo(HELLO);
}

Мы также можем преобразовать CompletableFuture в Future , используя метод fromCompletableFuture() .

6.10. Обработка исключений

В случае сбоя Future мы можем обработать ошибку несколькими способами.

Например, мы можем использовать метод recovery() для возврата другого результата, такого как сообщение об ошибке:

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

assertThat(future.get())
.isEqualTo("fallback value");
}

Или мы можем вернуть результат другого вычисления Future , используя recoveryWith() :

@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");
}

Метод fallbackTo() — это еще один способ обработки ошибок. Он вызывается для Future и принимает в качестве параметра другое Future .

Если первое Future успешно, то оно возвращает свой результат. В противном случае, если второй Future успешен, он возвращает свой результат. Если оба Future терпят неудачу, то метод failed() возвращает Future объекта Throwable , который содержит ошибку первого 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("String index out of range: -1");
}

7. Заключение

В этой статье мы увидели, что такое Future , и узнали некоторые из его важных концепций. Мы также рассмотрели некоторые функции API на нескольких практических примерах.

Полная версия кода доступна на GitHub .