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
CompletableFutureestableciendo 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 tipoT.
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>