Completablefuture

CompletableFuture

1. Introducción

  • CompletableFuture es una clase de Java (java.util.concurrent) introducida en Java 8 para programación asíncrona.
  • Representa un resultado futuro de una tarea asíncrona que puede ser completado manualmente.
  • Permite encadenar tareas, combinar resultados, manejar errores y ejecutar operaciones en paralelo.
  • Mejora la interfaz Future tradicional al ofrecer métodos no bloqueantes y mayor flexibilidad.

2. Creación de un CompletableFuture

Métodos estáticos comunes:

  • supplyAsync(Supplier<U> supplier):

    • Ejecuta una tarea asíncrona que devuelve un resultado (Supplier).
    • Usa el ForkJoinPool.commonPool() por defecto o un Executor personalizado.
    CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Hola");
  • runAsync(Runnable runnable):

    • Ejecuta una tarea asíncrona sin retorno (Runnable).
    CompletableFuture<Void> future = CompletableFuture.runAsync(() -> System.out.println("Ejecutando"));
  • completedFuture(value):

    • Crea un CompletableFuture ya completado con un valor.
    CompletableFuture<String> future = CompletableFuture.completedFuture("Resultado");

3. Métodos Básicos

  • Completar manualmente:
    • complete(T value): Completa el futuro con un valor.
    • completeExceptionally(Throwable ex): Completa el futuro con una excepción.
  • Obtener resultados:
    • get(): Bloquea hasta obtener el resultado (lanza InterruptedExceptionExecutionException).
    • join(): Similar a get(), pero lanza CompletionException (no requiere try-catch).

4. Encadenamiento de Tareas

Métodos para encadenar:

  • thenApply(Function<T, U>):

    • Transforma el resultado del futuro con una función.
    future.thenApply(s -> s + " Mundo"); // "Hola Mundo"
  • thenAccept(Consumer<T>):

    • Ejecuta una acción con el resultado (sin retorno).
    future.thenAccept(s -> System.out.println(s));
  • thenRun(Runnable):

    • Ejecuta una acción al finalizar (sin acceso al resultado).
    future.thenRun(() -> System.out.println("Finalizado"));
  • thenCompose(Function<T, CompletableFuture<U>>):

    • Para encadenar futuros anidados (evita CompletableFuture<CompletableFuture<T>>).
    future.thenCompose(s -> CompletableFuture.supplyAsync(() -> s + "!"));

Variantes Asíncronas:

  • thenApplyAsync()thenAcceptAsync(), etc.: Ejecutan la siguiente etapa en un hilo diferente.

5. Combinación de Futures

  • thenCombine(CompletableFuture<U>, BiFunction<T, U, V>):

    • Combina dos futuros cuando ambos completan.
    future1.thenCombine(future2, (a, b) -> a + b);
  • thenAcceptBoth(CompletableFuture<U>, BiConsumer<T, U>):

    • Ejecuta un Consumer con los resultados de ambos futuros.
  • runAfterBoth(CompletableFuture<?>, Runnable):

    • Ejecuta una acción cuando ambos futuros completan.
  • applyToEither(CompletableFuture<T>, Function<T, U>):

    • Procesa el resultado del futuro que complete primero.
    future1.applyToEither(future2, s -> s);

6. Manejo de Excepciones

  • exceptionally(Function<Throwable, T>):

    • Maneja excepciones y devuelve un valor por defecto.
    future.exceptionally(ex -> "Falló: " + ex.getMessage());
  • handle(BiFunction<T, Throwable, U>):

    • Maneja tanto éxito como error.
    future.handle((res, ex) -> ex != null ? "Error" : res);
  • whenComplete(BiConsumer<T, Throwable>):

    • Similar a handle, pero no transforma el resultado (solo para efectos secundarios).

7. Métodos de Utilidad

  • allOf(CompletableFuture<?>...):

    • Crea un futuro que completa cuando todos los futuros dados completan.
    CompletableFuture.allOf(future1, future2).join();
  • anyOf(CompletableFuture<?>...):

    • Completa cuando cualquiera de los futuros completa.

8. Ejemplos Prácticos

Ejemplo 1: Encadenamiento simple

CompletableFuture.supplyAsync(() -> 10)
    .thenApply(n -> n * 2)
    .thenAccept(System.out::println); // 20

Ejemplo 2: Combinación de futuros

CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Hola");
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "Mundo");

future1.thenCombine(future2, (a, b) -> a + " " + b)
       .thenAccept(System.out::println); // "Hola Mundo"

Ejemplo 3: Manejo de excepciones

CompletableFuture.supplyAsync(() -> {
    if (Math.random() > 0.5) throw new RuntimeException("Error");
    return "Éxito";
}).exceptionally(ex -> "Recuperación: " + ex.getMessage())
  .thenAccept(System.out::println);

9. Mejores Prácticas

  • Evitar bloqueos: Preferir métodos como thenApply/thenAccept en lugar de get().
  • Usar Executors personalizados: Para controlar recursos en aplicaciones intensivas.
  • Manejar excepciones: Siempre incluir exceptionally o handle en cadenas largas.
  • Evitar anidamiento excesivo: Usar thenCompose para futuros anidados.

10. Casos de Uso Comunes

  • Procesamiento paralelo de datos.
  • Composición de llamadas a APIs asíncronas.
  • Ejecución de tareas con tiempo límite (Java 9+ con orTimeout).
  • Pipeline de operaciones con manejo de errores.

11. Consideraciones Adicionales (Java 9+)

  • orTimeout(timeout, TimeUnit): Completa con TimeoutException si no finaliza en el tiempo.
  • completeOnTimeout(defaultValue, timeout, TimeUnit): Completa con un valor por defecto si hay timeout.