1. Inicio de una tarea asíncrona: supplyAsync y runAsync
La forma más común de iniciar una tarea asíncrona es usar CompletableFuture.supplyAsync. Este método acepta una lambda o un método que devuelve un resultado. Por ejemplo, queremos simular la carga de datos desde un servidor:
import java.util.concurrent.CompletableFuture;
public class Main {
public static void main(String[] args) {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
// Simulación de una operación larga (por ejemplo, la descarga de un archivo)
sleep(1000);
return "Datos del servidor";
});
System.out.println("¡Tarea iniciada!");
// ... aquí puedes hacer algo más mientras la tarea se ejecuta
}
private static void sleep(long ms) {
try { Thread.sleep(ms); } catch (InterruptedException ignored) {}
}
}
runAsync: cuando no se necesita resultado
Si tu tarea no devuelve nada (por ejemplo, solo escribe en el log, envía una notificación), usa runAsync:
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
sleep(500);
System.out.println("¡Operación completada!");
});
runAsync siempre devuelve CompletableFuture<Void>, porque no se espera un resultado.
2. thenApply, thenAccept, thenRun: ¿en qué se diferencian?
Cuando una tarea asíncrona termina, normalmente quieres hacer algo con el resultado. Para ello existen los métodos «manejadores»:
- thenApply — transforma el resultado y devuelve un nuevo resultado.
- thenAccept — consume el resultado, no devuelve nada (se usa para efectos secundarios).
- thenRun — no recibe el resultado y no devuelve nada (simplemente ejecuta una acción tras finalizar la tarea).
thenApply: procesar y transformar el resultado
Si necesitas transformar el resultado de la tarea anterior, usa thenApply. Por ejemplo, cargamos una cadena y ahora queremos saber su longitud:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Java");
CompletableFuture<Integer> lengthFuture = future.thenApply(s -> {
System.out.println("Calculando la longitud de la cadena...");
return s.length();
});
// lengthFuture ahora contiene un Integer: la longitud de la cadena "Java"
lengthFuture.thenAccept(len -> System.out.println("Longitud: " + len));
¿Qué ocurre?
- future contiene la cadena "Java".
- thenApply transforma la cadena en su longitud (int).
- thenAccept imprime el resultado.
thenAccept: actuar sobre el resultado (no devuelve nada)
Si solo necesitas hacer algo con el resultado (por ejemplo, mostrarlo en pantalla) y no hay que devolver nada — usa thenAccept:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "¡Hola, mundo!");
future.thenAccept(result -> {
System.out.println("Resultado: " + result);
});
thenAccept es como un «consumidor»: se come el resultado y hace algo útil con él.
thenRun: acción sin resultado
Si solo quieres ejecutar alguna acción tras finalizar la tarea y no necesitas el resultado, usa thenRun:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "¡Listo!");
future.thenRun(() -> {
System.out.println("¡Carga completada!");
});
Atención: dentro thenRun no puedes obtener el resultado de la tarea anterior — simplemente se ignora.
3. Cadenas de llamadas: construimos un pipeline de tareas
La mayor fortaleza de CompletableFuture es su capacidad para construir cadenas de cálculos. Cada método (thenApply, thenAccept, thenRun) devuelve un nuevo CompletableFuture, al que puedes volver a añadir un manejador.
Ejemplo: procesamiento en varias etapas
Mejoremos nuestra aplicación: cargaremos los datos, los transformaremos, mostraremos el resultado y escribiremos en el log que todo ha finalizado.
CompletableFuture.supplyAsync(() -> {
System.out.println("Paso 1: Cargando datos...");
sleep(500);
return "Java";
})
.thenApply(data -> {
System.out.println("Paso 2: Transformando los datos...");
return data.toUpperCase();
})
.thenAccept(result -> {
System.out.println("Paso 3: Mostrando el resultado: " + result);
})
.thenRun(() -> {
System.out.println("Paso 4: ¡Todo ha terminado!");
});
Salida en consola:
Paso 1: Cargando datos...
Paso 2: Transformando los datos...
Paso 3: Mostrando el resultado: JAVA
Paso 4: ¡Todo ha terminado!
Atención:
Cada paso siguiente comienza solo tras finalizar el anterior. Esto permite construir auténticos «pipelines» de procesamiento de datos.
4. Variantes asíncronas: thenApplyAsync, thenAcceptAsync, thenRunAsync
Por defecto, los manejadores (thenApply, thenAccept, thenRun) se ejecutan en el mismo hilo en el que terminó la tarea anterior. A veces no es conveniente — si el procesamiento es pesado, es mejor trasladarlo a otro hilo.
Para eso existen las versiones asíncronas:
- thenApplyAsync
- thenAcceptAsync
- thenRunAsync
¿En qué se diferencian?
- Sin Async: el manejador puede ejecutarse en el mismo hilo que la tarea anterior (por ejemplo, si la tarea terminó en ForkJoinPool, el manejador se ejecuta allí mismo).
- Con Async: el manejador se ejecutará garantizadamente en otro hilo del ForkJoinPool (o de tu propio Executor).
Ejemplo: comparemos un manejador normal y uno asíncrono
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
System.out.println("Cargando... [" + Thread.currentThread().getName() + "]");
return "Hello";
});
future.thenApply(result -> {
System.out.println("thenApply: [" + Thread.currentThread().getName() + "]");
return result + " World";
});
future.thenApplyAsync(result -> {
System.out.println("thenApplyAsync: [" + Thread.currentThread().getName() + "]");
return result + " Async World";
});
Salida típica:
Cargando... [ForkJoinPool.commonPool-worker-1]
thenApply: [ForkJoinPool.commonPool-worker-1]
thenApplyAsync: [ForkJoinPool.commonPool-worker-2]
Conclusión:
El manejador asíncrono se ejecuta en otro hilo.
¿Cuándo usar los métodos Async?
- Si el procesamiento es intensivo en recursos (por ejemplo, cálculos complejos, trabajo de red).
- Si no quieres bloquear el hilo en el que terminó la tarea anterior.
- Si quieres gestionar los hilos de forma explícita (por ejemplo, pasando tu propio Executor como segundo argumento).
5. Matices útiles
Tabla: comparación de los métodos thenApply, thenAccept, thenRun
| Método | ¿Usa el resultado? | ¿Devuelve valor? | Para qué usarlo |
|---|---|---|---|
|
Sí | Sí | Transformación del resultado |
|
Sí | No | Efectos secundarios (salida, registro) |
|
No | No | Acción simple tras finalizar la tarea |
|
Sí | Sí | Lo mismo, pero en otro hilo |
|
Sí | No | Lo mismo, pero en otro hilo |
|
No | No | Lo mismo, pero en otro hilo |
Pregunta: ¿cómo construir cadenas largas?
Puedes encadenar métodos uno tras otro, como si fueran piezas de LEGO:
CompletableFuture.supplyAsync(() -> "42")
.thenApply(Integer::parseInt)
.thenApply(x -> x * 2)
.thenAccept(x -> System.out.println("Resultado: " + x));
Salida:
Resultado: 84
Cada paso siguiente recibe el resultado del anterior; puede modificarlo o simplemente usarlo.
6. Errores típicos al trabajar con thenApply, thenAccept, thenRun
Error n.º 1: Confusión con los tipos de retorno.
thenApply debe devolver un valor que continúe por la cadena. Si usas por error thenApply, pero no devuelves resultado, la operación siguiente recibirá null (o ni siquiera compilará). Para efectos secundarios usa thenAccept o thenRun.
Error n.º 2: Intentar usar el resultado en thenRun.
Dentro de thenRun no hay acceso al resultado de la tarea anterior. Si quieres usar el resultado, elige thenApply o thenAccept.
Error n.º 3: Bloquear el hilo principal.
Si llamas a get() o join() en el hilo principal, pierdes todas las ventajas de la asincronía: el hilo esperará a que la tarea termine, como en el viejo y buen código síncrono. Es mejor usar cadenas no bloqueantes y callbacks.
Error n.º 4: No gestionar errores.
Si en la cadena se produce una excepción y no añadiste un manejador (exceptionally, handle, whenComplete), se «perderá», y la tarea puede finalizar con un error que no verás. Gestiona siempre los errores en las cadenas.
Error n.º 5: Ejecución inesperada en otro hilo.
Los métodos asíncronos (thenApplyAsync y otros) pueden ejecutarse en otro hilo. Si accedes a variables no protegidas para acceso concurrente, pueden aparecer condiciones de carrera.
GO TO FULL VERSION