Programación Asíncrona con CompletableFuture en Java

Java 8 introdujo la clase CompletableFuture, una herramienta poderosa para la programación asíncrona. Permite ejecutar tareas en hilos separados sin bloquear el hilo principal, mejorando la concurrencia y el rendimiento de las aplicaciones. Esta clase implementa las interfaces Future y CompletionStage, ofreciendo métodos flexibles para encadenar, combinar y gestionar el resultado de operaciones asíncronas, incluyendo un robusto manejo de excepciones.

Escenarios Comunes de Uso

CompletableFuture es particularmente útil en los siguientes escenarios:

  • Combinar dos operaciones asíncronas independientes donde la segunda depende del resultado de la primera.
  • Esperar la finalización de un conjunto de tareas asíncronas.
  • Obtener el resultado de la tarea asíncrona más rápida de un conjunto.
  • Completar manualmente una tarea CompletableFuture estableciendo su resultado o excepción.
  • Reaccionar a la finalización de una tarea asíncrona, utilizando su resultado para realizar operaciones subsiguientes sin bloqueo.

Métodos Clave y Ejemplos

Creación de Tareas Asíncronas: runAsync y supplyAsync

Estas métodos estáticos inician operaciones asíncronas. Si no se especifica un Executor, utilizan el ForkJoinPool.commonPool().

  • runAsync(Runnable runnable): Ejecuta una tarea que no devuelve un resultado (Void).
  • supplyAsync(Supplier<t> supplier)</t>: Ejecuta una tarea que devuelve un resultado de tipo T.
Ejemplo con runAsync:

CompletableFuture<void> tareaUno = CompletableFuture.runAsync(() -> {
    System.out.println("Ejecutando tarea uno en: " + Thread.currentThread().getName());
});
CompletableFuture<void> tareaDos = CompletableFuture.runAsync(() -> {
    System.out.println("Ejecutando tarea dos en: " + Thread.currentThread().getName());
});

// El hilo principal no se bloquea si no se llama a get()
// tareaUno.get();
// tareaDos.get();

System.out.println("Hilo principal: " + Thread.currentThread().getName());
</void></void>
Ejemplo con supplyAsync:

CompletableFuture<string> resultadoFuturo = CompletableFuture.supplyAsync(() -> {
    System.out.println("Generando resultado en: " + Thread.currentThread().getName());
    return "Resultado de la tarea";
});
CompletableFuture<string> otroResultadoFuturo = CompletableFuture.supplyAsync(() -> {
    System.out.println("Generando otro resultado en: " + Thread.currentThread().getName());
    return "Otro resultado";
});

// El método get() sí bloquea el hilo principal hasta obtener el resultado.
System.out.println(resultadoFuturo.get());
System.out.println(otroResultadoFuturo.get());
</string></string>

Manejo de Resultados y Excepciones: whenComplete, exceptionally, y handle

whenComplete

Ejecuta una acción cuando la tarea asíncrona se completa, ya sea con éxito o con una excepción. Las variantes Async ejecutan la acción en un hilo separado (usando el Executor especificado o el pool común).


CompletableFuture<string> operacion = CompletableFuture.supplyAsync(() -> {
    System.out.println("Operación principal en: " + Thread.currentThread().getName());
    return "Resultado Operación";
}).whenComplete((resultado, excepcion) -> {
    if (excepcion == null) {
        System.out.println("Completado con resultado: " + resultado + " en: " + Thread.currentThread().getName());
    } else {
        System.err.println("Operación fallida: " + excepcion.getMessage() + " en: " + Thread.currentThread().getName());
    }
}).whenCompleteAsync((resultado, excepcion) -> {
     System.out.println("Async callback en: " + Thread.currentThread().getName());
});
</string>
exceptionally

Proporciona una forma de manejar excepciones lanzadas durante la ejecución de la tarea asíncrona. Devuelve un nuevo CompletableFuture con el resultado de la función de manejo de excepciones.


CompletableFuture<string> futuroConExcepcion = CompletableFuture.supplyAsync(() -> {
    System.out.println("Intentando operación que falla en: " + Thread.currentThread().getName());
    int divisionPorCero = 1 / 0; // Provoca una ArithmeticException
    return "Resultado Exitoso";
}).exceptionally(ex -> {
    System.err.println("Error capturado: " + ex.getMessage());
    return "Valor por defecto en caso de error";
});

System.out.println(futuroConExcepcion.get()); // Obtendrá "Valor por defecto en caso de error"
</string>
handle

Similar a whenComplete, pero permite transformar el resultado o la excepción en un nuevo valor. Es útil cuando se necesita procesar tanto el resultado exitoso como la excepción.


CompletableFuture<string> futuroConHandle = CompletableFuture.supplyAsync(() -> {
    System.out.println("Operación para handle en: " + Thread.currentThread().getName());
    // int div = 1 / 0; // Descomentar para probar la rama de excepción
    return "Resultado OK";
}).handle((resultado, excepcion) -> {
    if (excepcion != null) {
        System.err.println("Handle detectó error: " + excepcion.getMessage());
        return "Resultado Fallido del Handle";
    }
    return resultado + " - Procesado por Handle";
});

System.out.println(futuroConHandle.get());
</string>

Encadenamiento de Operaciones: thenApply, thenAccept, thenRun

thenApply

Encadena una operación que depende del resultado de la tarea anterior. La función proporcionada toma el resultado de la tarea anterior y devuelve un nuevo resultado.


CompletableFuture<string> resultadoEncadenado = CompletableFuture.supplyAsync(() -> "Hola")
    .thenApply(s -> {
        System.out.println("Primer thenApply en: " + Thread.currentThread().getName());
        return s + " Mundo";
    })
    .thenApplyAsync(s -> {
        System.out.println("Segundo thenApplyAsync en: " + Thread.currentThread().getName());
        return s + "!";
    });

System.out.println(resultadoEncadenado.get()); // Salida: Hola Mundo!
</string>
thenAccept

Encadena una operación que consume el resultado de la tarea anterior, pero no devuelve un nuevo resultado (retorna CompletableFuture<Void>). No maneja excepciones de la tarea anterior explícitamente.


CompletableFuture<void> consumoResultado = CompletableFuture.supplyAsync(() -> "Datos")
    .thenAccept(datos -> {
        System.out.println("Consumiendo: " + datos + " en: " + Thread.currentThread().getName());
    });

consumoResultado.get(); // Asegura la ejecución del thenAccept
</void>
thenRun

Encadena una operación que se ejecuta después de que la tarea anterior se complete, pero no consume ni utiliza el resultado de la tarea anterior. Retorna CompletableFuture<Void>.


CompletableFuture<void> ejecucionSecuencial = CompletableFuture.supplyAsync(() -> {
    System.out.println("Tarea previa completada.");
    return "Algún Valor";
}).thenRun(() -> {
    System.out.println("Ejecutando acción posterior sin usar el resultado.");
});

ejecucionSecuencial.get();
</void>

Combinación de Múltiples CompletableFuture: thenCombine, thenAcceptBoth, applyToEither, acceptEither, runAfterEither, runAfterBoth

thenCombine

Combina los resultados de dos CompletableFuture independientes usando una BiFunction. La función se ejecuta solo después de que ambas tareas hayan finalizado.


CompletableFuture<integer> futuro1 = CompletableFuture.supplyAsync(() -> {
    System.out.println("Futuro 1 en: " + Thread.currentThread().getName());
    return 10;
});
CompletableFuture<integer> futuro2 = CompletableFuture.supplyAsync(() -> {
    System.out.println("Futuro 2 en: " + Thread.currentThread().getName());
    return 20;
});

CompletableFuture<integer> combinacion = futuro1.thenCombine(futuro2, (res1, res2) -> {
    System.out.println("Combinando en: " + Thread.currentThread().getName());
    return res1 + res2;
});

System.out.println("Resultado combinado: " + combinacion.get());
</integer></integer></integer>
thenAcceptBoth

Similar a thenCombine, pero consume los resultados de ambas tareas usando una BiConsumer, sin producir un nuevo resultado (retorna CompletableFuture<Void>).


CompletableFuture<integer> f1 = CompletableFuture.supplyAsync(() -> 10);
CompletableFuture<integer> f2 = CompletableFuture.supplyAsync(() -> 20);

CompletableFuture<void> consumoAmbos = f1.thenAcceptBoth(f2, (r1, r2) -> {
    System.out.println("Ambos resultados: " + r1 + ", " + r2);
});
consumoAmbos.get();
</void></integer></integer>
applyToEither

Aplica una función al resultado de la primera CompletableFuture que se complete. La otra tarea se cancelará si es posible.


CompletableFuture<integer> ft1 = CompletableFuture.supplyAsync(() -> {
    try { Thread.sleep(500); } catch (InterruptedException e) {}
    return 1;
});
CompletableFuture<integer> ft2 = CompletableFuture.supplyAsync(() -> {
    try { Thread.sleep(100); } catch (InterruptedException e) {}
    return 2;
});

CompletableFuture<integer> resultadoParcial = ft1.applyToEither(ft2, resultado -> {
    System.out.println("Procesando resultado de la más rápida: " + resultado);
    return resultado * 10;
});

System.out.println("Resultado de la más rápida: " + resultadoParcial.get()); // Probablemente 20
</integer></integer></integer>
acceptEither

Consume el resultado de la primera CompletableFuture que se complete usando un Consumer.


CompletableFuture<integer> ftA = CompletableFuture.supplyAsync(() -> { try { Thread.sleep(300); } catch (InterruptedException e) {} return 1; });
CompletableFuture<integer> ftB = CompletableFuture.supplyAsync(() -> { try { Thread.sleep(200); } catch (InterruptedException e) {} return 2; });

CompletableFuture<void> consumoParcial = ftA.acceptEither(ftB, resultado -> {
    System.out.println("Consumiendo resultado de la más rápida: " + resultado);
});
consumoParcial.get();
</void></integer></integer>
runAfterEither

Ejecuta un Runnable tan pronto como cualquiera de las dos CompletableFuture se complete, sin usar sus resultados.


CompletableFuture<integer> fA = CompletableFuture.supplyAsync(() -> { try { Thread.sleep(400); } catch (InterruptedException e) {} return 1; });
CompletableFuture<integer> fB = CompletableFuture.supplyAsync(() -> { try { Thread.sleep(100); } catch (InterruptedException e) {} return 2; });

CompletableFuture<void> accionPosterior = fA.runAfterEither(fB, () -> {
    System.out.println("Una de las tareas ha finalizado.");
});
accionPosterior.get();
</void></integer></integer>
runAfterBoth

Ejecuta un Runnable solo después de que ambas CompletableFuture se hayan completado.


CompletableFuture<integer> fX = CompletableFuture.supplyAsync(() -> 1);
CompletableFuture<integer> fY = CompletableFuture.supplyAsync(() -> 2);

CompletableFuture<void> accionFinal = fX.runAfterBoth(fY, () -> {
    System.out.println("Ambas tareas han finalizado.");
});
accionFinal.get();
</void></integer></integer>

Encadenamiento Complejo: thenCompose

Permite encadenar CompletableFuture donde la función que se aplica devuelve otro CompletableFuture. Esto es esencial para flujos de trabajo asíncronos donde cada paso puede iniciar una nueva operación asíncrona.


CompletableFuture<integer> primerPaso = CompletableFuture.supplyAsync(() -> {
    System.out.println("Iniciando primer paso...");
    return 5;
});

CompletableFuture<integer> resultadoFinal = primerPaso.thenCompose(valor -> {
    System.out.println("Procesando valor del primer paso: " + valor);
    // Devuelve otro CompletableFuture
    return CompletableFuture.supplyAsync(() -> {
        System.out.println("Iniciando segundo paso...");
        return valor + 10;
    });
});

System.out.println("Resultado del flujoCompose: " + resultadoFinal.get());
</integer></integer>

Etiquetas: java completablefuture programación asíncrona Java 8 concurrencia

Publicado el 9-18 06:15