9 Completando explícitamente un CompletableFuture¶
Introducción¶
Como hemos podido comprobar, un CompletableFuture
es una especie de contenedor de un resultado futuro. Hasta ahora hemos usado algún método estático para construir el CompletableFuture
indicando la tarea que debe ejecutar.
Sin embargo, tenemos otra posibilidad aún más flexible, que consiste en crear un objeto CompletableFuture
como un simple contenedor, sin asociarle inicialmente ninguna tarea, y posteriormente cuando se haya terminado de ejecutar la tarea deseada, se indique explícitamente que se ha completado el CompletableFuture
proporcionando el valor de resultado.
Si en un momento dado queremos saber si un CompletableFuture
ha sido completado o no, podemos llamar a su método isDone()
que retornará un valor booleano indicativo de si ya ha sido completado.
Con un valor¶
Para crear un CompletableFuture
de esta manera tan sólo tendremos que llamar a su constructor con el operador new
, igual que haríamos con cualquier otra clase. Cuando se desee indicar explícitamente que se ha completado el CompletableFuture
y ya tengamos el valor resultado disponible, llamaremos a su método complete(resultado)
.
En el siguiente ejemplo se crea una función que ejecuta una tarea asíncrona que retorna un CompletableFuture
que posteriormente es completado de forma explícita:
public static CompletableFuture<String> asyncGreet() {
CompletableFuture<String> completableFuture = new CompletableFuture<>();
Executors.newCachedThreadPool().submit(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
String greeting = "Quillo que";
System.out.printf("%s - Greeting - %s\n",
Thread.currentThread().getName(), greeting);
completableFuture.complete(greeting);
});
return completableFuture;
}
Y para dicha función haríamos, por ejemplo:
// Se construye la cadena de operaciones,
// que no es ejecutada hasta que no se completa explícitamente
// el CompletableFuture original.
CompletableFuture<String> completableFuture = asyncGreet();
completableFuture.thenAccept(greeting -> System.out.printf("%s - Consumer - %s\n",
Thread.currentThread().getName(), greeting));
System.out.printf("%s - Main\n", Thread.currentThread().getName());
Con un valor por defecto tras un tiempo máximo¶
Desde Java 9, podemos hacer que un CompletableFuture sea completado con un valor por defecto si transcurrido un determinado tiempo máximo (timeout) éste no ha sido completado de forma natural. Para ello usaremos el método completeOnTimeout(value, timeout, timeUnit)
sobre dicho CompletableFuture, que retornará un nuevo CompletableFuture que será completado con el valor (o excepción) del CompletableFuture anterior si éste es completado antes del timeout, o con el valor pasado como argumento si transcurrido dicho tiempo el CompletableFuture anterior no ha sido completado.
Veamos un ejemplo:
private void completeOnTimeoutExample() {
CompletableFuture<Void> cf =
CompletableFuture.supplyAsync(this::generateNumber)
.completeOnTimeout(100, 5, TimeUnit.SECONDS)
.thenAcceptAsync(this::printNumber);
System.out.printf("%s - Main\n", Thread.currentThread().getName());
cf.join();
}
private int generateNumber() {
System.out.printf("%s - Supplier\n", Thread.currentThread().getName());
int value = ThreadLocalRandom.current().nextInt(10) + 1;
sleep(value * 1000);
return value;
}
private void printNumber(Integer value) {
System.out.printf("%s - Consumer - %d\n",
Thread.currentThread().getName(), value);
}
private boolean sleep(long timeInMilis) {
try {
Thread.sleep(timeInMilis);
return true;
} catch (InterruptedException e) {
return false;
}
}
Con una excepción¶
Si queremos completar un CompletableFuture
pero generando una excepción en vez de retornando un valor, podemos hacer uso del método completeExceptionally(throwable)
, que recibe el Throwable
correspondiente a la excepción. Por ejemplo:
Con una excepción por cancelación¶
La tarea asíncrona también puede cancelarse explícitamente por algún motivo. Para ello usará el método cancel(false)
, que completará el CompletableFuture
generando la excepción CancellationException
.
Las siguientes operaciones de la cadena serán completadas con la excepción CompletionException
.
Veamos un ejemplo:
public CompletableFuture<String> asyncGreet() throws InterruptedException {
CompletableFuture<String> completableFuture = new CompletableFuture<>();
Executors.newCachedThreadPool().submit(() -> {
Thread.sleep(500);
// Evidentemente esto se haría sólo en unas determinadas circunstancias.
completableFuture.cancel(false);
return null;
});
return completableFuture;
}
Y al usarlo se generaría la excepción CancellationException
. Por ejemplo:
CompletableFuture<String> futuroResultado = saludoAsincronoConCancelacion();
// ...
// Esta línea puede generar CancellationException.
String resultado = futuroResultado.get();
Si en un momento dado queremos saber si un CompletableFuture
ha sido cancelado, podemos llamar a su método isCancelled()
que retornará un valor booleano indicativo de si ha sido cancelado.
Un aspecto muy importante es que la cancelación de un CompletableFuture no interrumpe el hilo que está ejecutando, simplemente completa el CompletableFuture con la excepción CancellationException
. Por este motivo, el valor que le pasemos al método cancel(boolean)
es totalmente irrelevante.
Con una excepción por timeout¶
En algunas ocasiones si un CompletableFuture no ha sido completado en un tiempo máximo deseamos completarlo con una excepción TimeoutException
. Para ello, desde Java 9, podemos usar el método orTimeout(timeout, timeUnit)
, que ejecutado sobre un CompletableFuture retorna un nuevo CompletableFuture que es completado con el valor (o excepción) del anterior si éste es completado antes del timeout especificado. En caso contrario es completado con la excepción TimeoutException
.
CompletableFuture<Integer> cf = CompletableFuture.supplyAsync(this::computeEndlessly)
.orTimeout(1, TimeUnit.SECONDS);
future.get();
Creación de un CompletableFuture ya completado¶
Si de antemano conocemos el resultado de debe producir el CompletableFuture
podemos simplificar aún más el proceso usando el método estático CompletableFuture.completedFuture(resultado)
, que creará para nosotros un CompletableFuture completado con el indicado. Por ejemplo:
Future<String> futuroResultado = CompletableFuture.completedFuture("Quillo que");
// ...
String resultado = futuroResultado.get();
De forma similar, tenemos disponible el método estático CompletableFuture.failedFuture(throwable)
, que creará un CompletableFuture completado con la excepción indicada.
Estos métodos son útiles principalmente para los tests.