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 InterruptedException, ExecutionException).
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.