CompletableFuture в Java

Наверное многим известно что в Java 8 появился класс CompletableFuture который расширяет Future<V> и реализует дополнительный интерфейс CompletionStage, это позволяет нам писать код похожий на промисы в JS и не думать об исключениях. Я раньше писал и Callable<V> и Executor'ах до этого, вот кстати эта статья https://megahub.me/hub/java?w=77. Давайте зарефакторим решение которое было в той статье. Минусы того решения в том, что когда мы вызываем метод .get() у Future<V> он блокирует текущий поток (другой Future<V> в это время ждет), так как мы не дошли еще до него. Соответственно код похож больше на синхронный, надо исправлять.

Для начала, поставим задачу:

Нам нужно сделать микро приложение которое сможет асинхронно считать факториал и гипотенузу. 

Начнем с проектирования:

У нас будет несколько классов:

  1. Hypotenuse
  2. Factorial
  3. AsyncCalculator
  4. Result 

Давайте начнем с Hypotenuse, этот класс должен принимать значение двух катетов и возвращать нам гипотенузу из этих двух катетов. Формула следующая:

c^2 = a^2 + b^2

Квадрат гипотенузы равен сумме квадратов катетов

Давайте напишем код для этого класса:

public class Hypotenuse {

private final int a;
private final int b;

Hypotenuse(int a, int b) {
this.a = requirePositive(a, "a");
this.b = requirePositive(b, "b");
}

private static int requirePositive(int v, String catheterName) {
if (v < 0) {
throw new IllegalArgumentException("catheter " + catheterName + " must have positive value");
}
return v;
}

int calculate() {
return (int) Math.ceil(Math.sqrt((a * a) + (b * b)));
}
}

Тут я думаю все просто, объяснять ничего не придется.

Теперь давайте напишем класс Factorial:

class Factorial {
private final int number;

Factorial(int number) {
if (number > 10 || number < 1) {
throw new IllegalArgumentException("number should be <= 10 and should be >= 1");
}

this.number = number;
}

long calculate() {
return IntStream.rangeClosed(1, number).reduce(1, (a, b) -> a * b);
}
}

Тут тоже все просто, считаем факториал через Stream API, ожидаем на вход число для которого нужно посчитать факториал.

Теперь напишем еще один простой класс и перейдем к вишенке на торте. Давайте напишем класс Result

class Result {

private final long factorial;
private final int hypotenuse;

Result(long factorial, int hypotenuse) {
this.factorial = factorial;
this.hypotenuse = hypotenuse;
}

long getFactorial() {
return factorial;
}

int getHypotenuse() {
return hypotenuse;
}

private long sumOfFactorialAndHypotenuse() {
return factorial + hypotenuse;
}

String getAsFormattedString() {
return String.format("Факториал: %16d%nГипотенуза: %13d%nСумма всех значений: %6d", factorial, hypotenuse, sumOfFactorialAndHypotenuse());
}
}

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

Давайте напишем класс который уже будет делать всю самую интересную работу. Приступим

class AsyncCalculator {
/*
* Этот класс делает вычисления асинхронными с помощью
* Java CompletableFuture класса.
*/

private final Factorial factorial;
private final Hypotenuse hypotenuse;

AsyncCalculator(Factorial factorial, Hypotenuse hypotenuse) { (1)
this.factorial = factorial;
this.hypotenuse = hypotenuse;
}

Result calculate() {
return CompletableFuture.supplyAsync(factorial::calculate) (2)
.thenCombineAsync(CompletableFuture.supplyAsync(hypotenuse::calculate), Result::new) (3)
.join(); (4)
}
}

Разберем этот класс:

(1) - Конструктор который принимает класс представляющий факториал и класс представляющий гипотенузу

(2) - Мы создаем CompletableFuture который принимает лямбду с которую нужно выполнить асинхронно. В данном месте мы передаем ее через method reference на метод calculate из класса Factorial. .supplyAsync создает CompletableFuture который будет выполнять переданную ему лямбду асинхронно, если не указывать executor service, то выполняться она будет по-умолчанию в стандартном Fork/Join пуле.

(3) Мы комбинируем первый CompletableFuture еще с одним, который считает гипотенузу, принцип тот же что и во втором пункте, добавляется только второй аргумент, это аггрегатор, он будет аггрегировать результат от первого CompletableFuture, со вторым, в нашем случае мы просто передаем ссылку на конструктор класса Result, когда оба вычисления закончатся, то будет создан класс Result

(4) Методом .join() мы сообщаем что хотим ожидать выполнения всех вычислений и затем вернуть результат.

А теперь о плюсах:

  1. Вызовы теперь полностью не блокирующие, две операции вычисления могут выполняться независимо и после выполнения обеих просто аггрегируются в определенный результат, в нашем случае класс Result
  2. Нет никаких исключений (мы ничего не ловим)
  3. Меньше кода чем с обычными Executor сервисами и обычными Future с Callable

Так же можно поиметь дополнительные плюсы в виде промежуточных обработок и возможностью завершения с исключением, но это уже не в этой статье. Советую посмотреть на методы которые доступны в CompletionStage.

Исходный код всего приложения можно найти на github

 

Java
08.08.2018
1 ответ
авторизуйтесь чтобы ответить