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 .