Skip to content

10 CompletableFuture.allOf()

Introducción

Podemos crear un CompletableFuture que sea completado cuando sean completados n CompletableFutures pasados como argumento. Para ello usaremos el método estático CompletableFuture.allOf(completableFuture...).

Este método retorna un CompletableFuture<Void>, por lo que no tenemos acceso a través de él a los valores con los que se han completado los CompletableFuture pasados como argumento. Si queremos acceder a dichos valores, deberemos usar el método join() individualmente sobre cada uno de dichos CompletableFuture.

Si cualquiera de los CompletableFuture pasados como argumentos se completa con una excepción, allOf() esperará a que se completen el resto de los CompletableFuture recibidos.

De esta manera el objetivo de allOf() es básicamente una manera de detectar que una serie de CompletableFuture han sido completados, sin tener en cuenta los valores con los que se completan o si se completan con una excepción.

Mejorando CompletableFuture.allOf()

Tal y como está diseñado, el método estático CompletableFuture.allOf() presenta varias decisiones de diseño algo controvertidas, con el objetivo de hacerlo lo más general posible:

  • Acepta como argumento objetos CompletableFuture<?> de distintos tipos (de ahí el carácter comodín ?).
  • Retorna un CompletableFuture<Void>, es decir que el CompletableFuture no contendrá los resultados de los CompletableFuture pasados como argumento.
  • Define como parámetro un vararg, en vez de una <List<CompletableFuture<?>>, lo que hace algo más complejo su uso cuando queremos pasarle una lista de CompletableFutures.

Sin embargo, en algunas ocasiones, los CompletableFutures que queremos pasarle al método allOf() son del mismo tipo de resultado. Para esos casos, podemos crear una versión específica de allOf() con un diseño menos general pero más limpio.

Así, vamos a hacer que la nueva versión de nuestro método reciba una Collection<CompletableFuture<T>>, es decir una colección de CompletableFutures del mismo tipo (como por ejemplo una lista). Además, el método retornará un CompletableFuture con la lista de resultados de los CompletableFuture de la lista pasada como argumento, es decir, un CompletableFuture<List<T>>:

public static <T> CompletableFuture<List<T>> allOf(
Collection<CompletableFuture<T>> futures
) {
    // ...
}

El método debe retornar un CompletableFuture que será completado cuando se hayan completado todos los CompletableFuture de la colección recibida como argumento. Esto ya lo hace la versión original de allOf(), pero nosotros ahora vamos a hacer que en vez de retornar un CompletableFuture<Void>, retorne un CompletableFuture con la lista de resultados de los CompletableFuture recibidos:

public static <T> CompletableFuture<List<T>> allOf(
Collection<CompletableFuture<T>> futures
) {
    return
        // Convertimos la colección en un stream.
        futures.stream()
        // Lo recolectamos a una lista y una vez recolectado...
        .collect(collectingAndThen(toList(),
            // ...debemos esperar a que todos los CompletableFuture de
            // la colección se hayan completado, para lo que usamos el
            // método allOf() original.
            l -> CompletableFuture.allOf(
                // Como recibe una vararg, convertimos la lista
                // en un array de CompletableFuture<T> para pasárselo
                // al método (un vararg es como un array)
                l.toArray(new CompletableFuture[0])
            )
            // Una vez se han completados todos los CompletableFuture
            // recibidos extraemos los resultados de todos ellos y
            // retornamos un CompletableFuture con la lista de 
            // resultados.
            .thenApply(nada ->
                // La lista se convierte en un stream.
                l.stream()
                    // Para cada CompletableFuture de la lista se
                    // obtiene sus valor.
                    .map(cf -> cf.join())
                    // Se recolecta el stream hacia una lista.
                    .collect(Collectors.toList())
            )
        ));
}

Por otra parte, y como ya hemos comentado, si cualquiera de los CompletableFuture pasados como argumentos se completa con una excepción, allOf() esperará a que se completen el resto de los CompletableFuture recibidos.

Sin embargo, en algunas ocasiones querremos hacer un cortocircuito, es decir, que en cuanto alguno de los CompletableFuture recibidos se complete con una excepción, el propio CompletableFuture retornado se complete inmediatamente con una excepción. Para ello vamos a crear una nueva versión del método, que vamos a llamar allOfOrException():

public static <T> CompletableFuture<List<T>> allOfOrException(
    Collection<CompletableFuture<T>> futures
) {
    // Creamos el CompletableFuture que vamos a retornar
    // llamado a la versión mejorada del método.
    CompletableFuture<List<T>> result = allOf(futures);
    // En cuanto alguno de los recibido se completa con una excepción
    // nosotros completamos con una excepción el que vamos a retornar
    for (CompletableFuture<?> f : futures) {
        f.handle((__, ex) -> 
            ex == null || result.completeExceptionally(ex)
        );
    }
    return result;
}

anyOf()

Si lo que queremos es obtener el valor con el que se ha completado el primero en completarse de entre n CompletableFutures, podemos usar el método estático CompletableFuture.anyOf(completableFuture...), que retorna un CompletableFuture<Object> que será completado con el valor con el que se completado el primero en completarse de los CompletableFutures pasados como argumento.

Si todos los CompletableFuture a recibir son del mismo tipo, entonces podemos crear una versión mejorada de anyOf(), que reciba una lista de CompletableFuture u que retorne un CompletableFuture con el valor retornado por el primero en completarse:

public static <T> CompletableFuture<T> anyOf(
    List<CompletableFuture<T>> cfs
) {
    return 
        CompletableFuture.anyOf(cfs.toArray(new CompletableFuture[0]))
        .thenApply(o -> (T) o);
}

con la ventaja de que en vez de retornar un CompletableFuture<Object>, retorna directamente un CompletableFuture<T>.