1. Avvio di un’attività asincrona: supplyAsync e runAsync
Il modo più comune per avviare un’attività asincrona è usare CompletableFuture.supplyAsync. Questo metodo accetta una lambda o un metodo che restituisce un risultato. Per esempio, vogliamo simulare il caricamento di dati da un server:
import java.util.concurrent.CompletableFuture;
public class Main {
public static void main(String[] args) {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
// Imitazione di un'operazione lunga (ad esempio, il download di un file)
sleep(1000);
return "Dati dal server";
});
System.out.println("Attività avviata!");
// ... qui puoi fare qualcos'altro mentre l'attività è in esecuzione
}
private static void sleep(long ms) {
try { Thread.sleep(ms); } catch (InterruptedException ignored) {}
}
}
runAsync: quando il risultato non serve
Se la tua attività non restituisce nulla (per esempio, scrive solo nel log, invia una notifica), usa runAsync:
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
sleep(500);
System.out.println("Operazione completata!");
});
runAsync restituisce sempre CompletableFuture<Void>, perché non è previsto alcun risultato.
2. thenApply, thenAccept, thenRun: qual è la differenza?
Quando un’attività asincrona si è conclusa, di solito vuoi fare qualcosa con il risultato. Per questo esistono i metodi «handler»:
- thenApply — trasforma il risultato e restituisce un nuovo risultato.
- thenAccept — accetta il risultato, non restituisce nulla (usato per effetti collaterali).
- thenRun — non accetta alcun risultato, non restituisce nulla (esegue semplicemente un’azione dopo il completamento dell’attività).
thenApply: elaborazione e trasformazione del risultato
Se devi trasformare il risultato dell’attività precedente, usa thenApply. Per esempio, abbiamo caricato una stringa e ora vogliamo conoscerne la lunghezza:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Java");
CompletableFuture<Integer> lengthFuture = future.thenApply(s -> {
System.out.println("Calcoliamo la lunghezza della stringa...");
return s.length();
});
// lengthFuture ora contiene un Integer — la lunghezza della stringa "Java"
lengthFuture.thenAccept(len -> System.out.println("Lunghezza: " + len));
Cosa succede:
- future contiene la stringa "Java".
- thenApply trasforma la stringa nella sua lunghezza (int).
- thenAccept stampa il risultato.
thenAccept: azione sul risultato (non restituisce nulla)
Se bisogna semplicemente fare qualcosa con il risultato (per esempio, stamparlo a schermo) e non è necessario restituire nulla, usa thenAccept:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Ciao, mondo!");
future.thenAccept(result -> {
System.out.println("Risultato: " + result);
});
thenAccept è come un «consumer»: consuma il risultato e ci fa qualcosa di utile.
thenRun: azione senza risultato
Se vuoi semplicemente eseguire un’azione dopo il completamento dell’attività, ma il risultato non ti serve, usa thenRun:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Fatto!");
future.thenRun(() -> {
System.out.println("Caricamento completato!");
});
Attenzione: all’interno di thenRun non puoi ottenere il risultato dell’attività precedente — viene semplicemente ignorato.
3. Catene di chiamate: costruiamo una pipeline di attività
La forza maggiore di CompletableFuture è la capacità di costruire catene di calcoli. Ogni metodo (thenApply, thenAccept, thenRun) restituisce un nuovo CompletableFuture a cui puoi aggiungere di nuovo un handler.
Esempio: elaborazione multistadio
Miglioriamo la nostra applicazione: carichiamo i dati, li trasformiamo, stampiamo il risultato e scriviamo nel log che tutto è completato.
CompletableFuture.supplyAsync(() -> {
System.out.println("Passo 1: Carichiamo i dati...");
sleep(500);
return "Java";
})
.thenApply(data -> {
System.out.println("Passo 2: Trasformiamo i dati...");
return data.toUpperCase();
})
.thenAccept(result -> {
System.out.println("Passo 3: Stampiamo il risultato: " + result);
})
.thenRun(() -> {
System.out.println("Passo 4: Tutto completato!");
});
Output su console:
Passo 1: Carichiamo i dati...
Passo 2: Trasformiamo i dati...
Passo 3: Stampiamo il risultato: JAVA
Passo 4: Tutto completato!
Nota bene:
Ogni passo successivo inizia solo dopo il completamento del precedente. Questo consente di costruire veri «pipeline» di elaborazione dei dati.
4. Varianti asincrone: thenApplyAsync, thenAcceptAsync, thenRunAsync
Per impostazione predefinita gli handler (thenApply, thenAccept, thenRun) vengono eseguiti nello stesso thread in cui si è conclusa l’attività precedente. A volte non è comodo — se l’elaborazione è pesante, è meglio spostarla in un thread separato.
Per questo esistono le versioni asincrone:
- thenApplyAsync
- thenAcceptAsync
- thenRunAsync
Qual è la differenza?
- Senza Async: l’handler può essere eseguito nello stesso thread dell’attività precedente (ad esempio, se l’attività è stata completata nel ForkJoinPool, l’handler verrà eseguito lì).
- Con Async: l’handler verrà sicuramente eseguito in un altro thread dal ForkJoinPool (o da un tuo Executor).
Esempio: confrontiamo un handler normale e uno asincrono
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
System.out.println("Caricamento... [" + 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";
});
Output tipico:
Caricamento... [ForkJoinPool.commonPool-worker-1]
thenApply: [ForkJoinPool.commonPool-worker-1]
thenApplyAsync: [ForkJoinPool.commonPool-worker-2]
Conclusione:
L’handler asincrono viene eseguito in un altro thread.
Quando usare i metodi Async?
- Se l’elaborazione è dispendiosa in termini di risorse (per esempio, calcoli complessi, lavoro di rete).
- Se non vuoi bloccare il thread in cui si è conclusa l’attività precedente.
- Se vuoi gestire esplicitamente i thread (ad esempio, passando un tuo Executor come secondo argomento).
5. Sfumature utili
Tabella: confronto dei metodi thenApply, thenAccept, thenRun
| Metodo | Usa il risultato? | Restituisce un valore? | Per cosa usarlo |
|---|---|---|---|
|
Sì | Sì | Trasformazione del risultato |
|
Sì | No | Effetti collaterali (stampa, logging) |
|
No | No | Semplice azione dopo il completamento dell’attività |
|
Sì | Sì | Lo stesso, ma in un altro thread |
|
Sì | No | Lo stesso, ma in un altro thread |
|
No | No | Lo stesso, ma in un altro thread |
Domanda: come costruire catene lunghe?
Puoi chiamare i metodi uno dopo l’altro, come in un set LEGO:
CompletableFuture.supplyAsync(() -> "42")
.thenApply(Integer::parseInt)
.thenApply(x -> x * 2)
.thenAccept(x -> System.out.println("Risultato: " + x));
Output:
Risultato: 84
Ogni passo successivo riceve il risultato del precedente, può modificarlo o semplicemente usarlo.
6. Errori tipici nell’uso di thenApply, thenAccept, thenRun
Errore n. 1: Confusione sui tipi dei valori restituiti.
thenApply deve restituire un valore che andrà avanti nella catena. Se usi per sbaglio thenApply ma non restituisci alcun risultato, l’operazione successiva riceverà null (o il codice non compilerà affatto). Per gli effetti collaterali usa thenAccept o thenRun.
Errore n. 2: Tentativo di usare il risultato in thenRun.
All’interno di thenRun non c’è accesso al risultato dell’attività precedente. Se vuoi usare il risultato, scegli thenApply o thenAccept.
Errore n. 3: Blocco del thread principale.
Se chiami get() o join() nel thread principale, perdi tutti i vantaggi dell’asincronia: il thread aspetterà il completamento dell’attività, come nel vecchio codice sincrono. Meglio usare catene non bloccanti e callback.
Errore n. 4: Mancata gestione degli errori.
Se nella catena si verifica un’eccezione e non aggiungi un gestore (exceptionally, handle, whenComplete), l’errore “andrà perso” e l’attività potrebbe terminare con un errore che non vedrai. Gestisci sempre gli errori nelle catene.
Errore n. 5: Esecuzione inattesa in un altro thread.
I metodi asincroni (thenApplyAsync e altri) possono essere eseguiti in un altro thread. Se accedi a variabili non protette per l’accesso concorrente, possono verificarsi race condition.
GO TO FULL VERSION