В прошлой статье был рассмотрен такой пример для работы с асинхронностью в языке Java, как тип CompletableFuture, который позволяет устанавливать коллбеки для обработки результатов асинхронной задачи. Однако что, если при вычислениях возникнет ошибка? Например, у нас есть следующая задача по вычислению факториала, где, если передаваемое число меньше 1, то генерируется ошибка:
Supplier<Integer> factorialTask = () -> {
// при некорректных данных генерируем ошибку
if(number < 1) throw new RuntimeException("Number must be greater than 0");
int n = 1;
int result = 1;
while(n <= number) result *= n++;
return result;
};
Как мы можем обработать эту ошибку? Для обработки ошибок в API класса CompletedFuture определен ряд методов. Рассмотрим ряд из них:
CompletableFuture<T> exceptionally(Function<Throwable, ? extends T> fn) CompletableFuture<T> exceptionallyCompose(Function<Throwable, ? extends CompletionStage<T>> fn)
Эти методы позволяют установить обратный вызов, который выполняется при возникновении ошибки на предыдущем этапе:
exceptionally(): вычисляет результат на основе ошибки, если она возникла на предыдущем этапе. То есть фактически обратный вызов производит переход Throwable -> T
exceptionallyCompose(): вызывает функцию для исключения, если она возникла на предыдущем этапе, и выполняет возвращенный результат - объект CompletableFuture.
То есть фактически обратный вызов производит переход Throwable -> CompletableFuture<T>
Рассмотрим на примере вычисления факториала:
import java.util.concurrent.*;
import java.util.function.Supplier;
import java.util.function.Function;
import java.util.function.Consumer;
class Program{
public static void main(String[] args) throws Exception{
System.out.println("Main thread started...");
int number = -5; // исходное число для вычисления факториала
// определяем задачу, которая вычисляет факториал
Supplier<Integer> factorialTask = () -> {
if(number < 1) throw new RuntimeException("Number must be greater than 0");
int n = 1;
int result = 1;
while(n <= number) result *= n++;
return result;
};
// задача для вывода финального результата
Consumer<Integer> printTask = result -> System.out.printf("Final result: %d\n", result);
// задача для обработки ошибки
Function<Throwable, Integer> printException = ex -> {
System.out.println(ex.getMessage());
throw new RuntimeException(ex.getMessage());
};
// определяем объект Executor, который будет выполнять задачи
ExecutorService executor = Executors.newCachedThreadPool();
CompletableFuture
.supplyAsync(factorialTask, executor)
.exceptionally(printException)
.thenAccept(printTask);
System.out.println("Main thread works...");
System.out.println("Main thread finished...");
executor.close(); // закрываем исполнителя
}
}
Рассмотрим ключевые моменты. Прежде всего основная задача, которая выполняется с помощь CompletedFuture - это задача вычисления факториала factorialTask. Эта задача возвращает целое число (значение Integer)
Для выполнения задачи и вызова коллбеков, обрабатывающих ее результат, определяем следующий код:
CompletableFuture
.supplyAsync(factorialTask, executor)
.exceptionally(printException)
.thenAccept(printTask);
supplyAsync(factorialTask, executor)
Устанавливаем выполняемую асинхронную задачу и объект Executor, который выполняет эту задачу.
Если передачаемое для вычисления факториала число некорректно, то генерируем исключение
if(number < 1) throw new RuntimeException("Number must be greater than 0");
exceptionally(printException)
Для обработки исключения определяем задачу printException, которая просто выводит информацию об исключении на консоль:
Function<Throwable, Integer> printException = ex -> {
System.out.println(ex.getMessage());
throw new RuntimeException(ex.getMessage());
};
Здесь стоит отметить, что поскольку задача факториала возвращает значение типа Integer, то и эта задача должна возвращать значение типа Integer. Однако в данном случае мы ничего не возвращаем и просто повторно генерируем исключение (причем непроверяемое исключение). Повторная генерация исключения позволит избежать выполнения следующего колбека, для которого требуется результат вычисления факториала.
thenAccept(printTask)
Задача printTask принимает результат - результат вычисления факториала и просто выводит его на консоль
В программе входное число является отрицательным:
int number = -5;
Соответственно при выполнении мы столкнемся с ошибкой и получим консольный вывод наподобие следующего:
Main thread started... java.lang.RuntimeException: Number must be greater than 0 Main thread works... Main thread finished...
И тут надо обратить внимание, что последний коллбек - printTask не выполняется, а выполнение основного потока не прерывается, хотя мы повторно генерируем исключение при его обработке.
Если же входное число было бы больше 0:
int number = 5;
то мы увидим результат работы коллбека из вызова thenAccept(printTask), а коллбек из вызова exceptionally(printException) не будет выполняться:
Main thread started... Final result: 120 Main thread works... Main thread finished...
Естественно это не единственно возможная схема обработки ошибок. Например, в примере выше мы генерировали повторно исключение. Но мы могли бы и сгенерировать результат, если переданное значение генерировало ошибку:
import java.util.concurrent.*;
import java.util.function.Supplier;
import java.util.function.Function;
import java.util.function.Consumer;
class Program{
public static void main(String[] args) throws Exception{
int age = -5;
// определяем задачу, которая вычисляет факториал
Supplier<Integer> validateAge = () -> {
if(age > 110 || age < 1) throw new RuntimeException("Age is invalid");
return age;
};
// задача для вывода финального результата
Consumer<Integer> printAge = result -> System.out.printf("Final age: %d\n", result);
// задача для обработки ошибки
Function<Throwable, Integer> printException = ex -> {
System.out.println(ex.getMessage());
return 18; // возвращаем некоторое значение по умолчанию
};
// определяем объект Executor, который будет выполнять задачи
ExecutorService executor = Executors.newCachedThreadPool();
CompletableFuture
.supplyAsync(validateAge, executor)
.exceptionally(printException)
.thenAccept(printAge);
// закрываем исполнителя
executor.close();
}
}
Здесь мы сначала проверяем возраст - значение переменной age в задаче validateAge: если оно не входит в неготорый диапазон, генерируем исключение:
Supplier<Integer> validateAge = () -> {
if(age > 110 || age < 1) throw new RuntimeException("Age is invalid");
return age;
};
Если генерируется исключение, то вызывается задача printException:
Function<Throwable, Integer> printException = ex -> {
System.out.println(ex.getMessage());
return 18; // возвращаем некоторое значение по умолчанию
};
Здесь также выводим сообщение об ошибке, но при этом возвращаем некоторое значение по умолчанию.
Таким образом при определении следующей цепочки:
CompletableFuture
.supplyAsync(validateAge, executor)
.exceptionally(printException)
.thenAccept(printAge);
Если в validateAge возникает исключение, оно передается в задачу printException, которая генерирует конечный результат. И этот результа передается в задачу printAge. То есть в данном случае последний коллбек - printAge выполняется в любом случае, даже если возникает ошибка. Консольный вывод в данном случае
java.lang.RuntimeException: Age is invalid Final age: 18
Исключение, которое генерируется в задачах CompletedFuture представляет тип CompletionException, которое наследуется от RuntimeException и фактически является оборткой над исключением.
Таким образом, когда мы генерируем исключение
if(age > 110 || age < 1) throw new RuntimeException("Age is invalid");
В реальности в задачу для обработки исключения из вызова exceptionally() передается не сгенерированное исключения RuntimeException, а обертка над ним в виде объекта CompletionException.
Поэтому в сообщении об исключении мы видим изначальный тип исключения - "java.lang.RuntimeException: Age is invalid". Чтобы обратиться к этому внутреннему исключению, мы можем использовать метод
getCause(), который определен в интерфейсе Throwble:
Function<Throwable, Integer> printException = ex -> {
System.out.println(ex.getCause().getMessage());
return 18; // возвращаем некоторое значение по умолчанию
};
Соответственно теперь при выводе на консоль не будет указываться тип исключения:
Age is invalid Final age: 18
Асинхронные задачи объекта CompletableFuture могут завершиться двумя способами: с результатом или с неперехваченным исключением. Для обработки обоих случаев можно использовать метод whenComplete():
CompletableFuture<T> whenComplete(BiConsumer<? super T, ? super Throwable> action)
В качестве параметра этот метод принимает действие, которое соответствует методу accept() функционального интерфейса BiConsumer<T,U>:
void accept(T t, U u)
То есть действие ничего не возвращает и принимает два параметра, причем второй параметр должен представлять объект Throwable. В случае с CompletableFuture
первый параметр будет представлять результат асинхронной задачи, а второй параметр - исключение, если оно возвникло в процессе выполнения асинхронной задачи.
Если результат был успешно вычислен, а исключения не возникло, то второй параметр равен null. Если же наоборот - при выполнении задачи возникло исключение, то первый параметр равен
null
Общая схема работы коллбека, который передается в данный метод, выглядит следующим образом:
f.whenComplete((result, error) -> {
if (error == null){ // если исключения нет
обрабатывает результат result;
}
else { // если возникло исключение
обрабатываем ошибку error;
}
});
Рассмотрим на примере вычисления факториала:
import java.util.concurrent.*;
import java.util.function.Supplier;
import java.util.function.BiConsumer;
class Program{
public static void main(String[] args) throws Exception{
System.out.println("Main thread started...");
// определяем объект Executor, который будет выполнять задачи
ExecutorService executor = Executors.newCachedThreadPool();
work(executor, 5);
work(executor, -5);
System.out.println("Main thread finished...");
executor.close();
}
static void work(Executor executor, int number){
// определяем задачу, которая вычисляет факториал
Supplier<Integer> factorialTask = () -> {
if(number < 1) throw new RuntimeException("Number must be greater than 0. Invalid number: " + number);
int n = 1;
int result = 1;
while(n <= number) result *= n++;
return result;
};
BiConsumer<Integer, Throwable> completedTask = (result, ex) -> {
if (ex == null) // если нет исключения
System.out.printf("factorial of %d is %d\n", number, result);
else // если возникло исключение
System.out.println(ex.getCause().getMessage());
};
CompletableFuture
.supplyAsync(factorialTask, executor)
.whenComplete(completedTask);
}
}
Основные действия здесь выполняются в методе work(), который принимает число для вычисления факториала и объект Executor для выполнения задач.
Сначала определяем собственно задачу для вычисления факториала:
Supplier<Integer> factorialTask = () -> {
if(number < 1) throw new RuntimeException("Number must be greater than 0. Invalid number: " + number);
int n = 1;
int result = 1;
while(n <= number) result *= n++;
return result;
};
Если переданное число здесь меньше 1, то генерируем исключение.
Для обработки завершения задачи (вне зависимости успешного или неудачного) определяем отдельную задачу типа BiConsumer:
BiConsumer<Integer, Throwable> completedTask = (result, ex) -> {
if (ex == null) // если нет исключения
System.out.printf("factorial of %d is %d\n", number, result);
else // если возникло исключение
System.out.println(ex.getCause().getMessage());
};
Поскольку результат вычисления факториала представляет тип Integer, то параметры задачи представляют типы Integer и Throwable. В целях демонстрации задача просто
выводит результат или сообщение об ошибке на консоль.
В конце делаем цепочку вызовов, в которые передаем выше определенные коллбеки:
CompletableFuture
.supplyAsync(factorialTask, executor)
.whenComplete(completedTask);
В методе main() вызываем метод work(), передавая в него для тестирования корркетные и некорректные значения:
ExecutorService executor = Executors.newCachedThreadPool(); work(executor, 5); work(executor, -5);
В итоге мы получим консольный вывод типа следующего (порядок вывода сообщение не детерминирован):
Main thread started... factorial of 5 is 120 Main thread finished... Number must be greater than 0. Invalid number: -5