1. Combiniamo CompletableFuture
Nella vita reale raramente tutto arriva da un’unica fonte: il profilo dell’utente e i suoi ordini si possono caricare in parallelo, i dati di due microservizi — unire, e talvolta si vuole elaborare semplicemente la prima risposta arrivata. L’approccio sincrono costringe ad attendere in sequenza; la classe CompletableFuture consente di avviare tutto contemporaneamente e combinare i risultati in modo elegante. Fornisce metodi specifici per questo: thenCombine, allOf, anyOf. Vediamo ciascuno in ordine.
Attività parallele: due richieste asincrone
Come apparirebbe in modo sincrono:
String name = loadUserName(); // lento
int balance = loadUserBalance(); // lento
System.out.println("Nome: " + name + ", Saldo: " + balance);
Problema: la seconda chiamata inizierà solo dopo il completamento della prima.
Metodo asincrono
Con CompletableFuture è possibile avviare entrambe le attività contemporaneamente:
CompletableFuture<String> nameFuture = CompletableFuture.supplyAsync(() -> loadUserName());
CompletableFuture<Integer> balanceFuture = CompletableFuture.supplyAsync(() -> loadUserBalance());
Ma come ottenere entrambi i risultati e gestirli insieme? A questo serve thenCombine.
2. thenCombine: uniamo i risultati di due attività
Il metodo thenCombine consente di combinare due CompletableFuture ed eseguire un’azione quando entrambe le attività sono terminate. Restituisce un nuovo CompletableFuture con il risultato della combinazione.
Firma:
<A, B, C> CompletableFuture<C> thenCombine(
CompletionStage<? extends B> other,
BiFunction<? super A, ? super B, ? extends C> fn
)
- A — tipo del risultato del primo future,
- B — tipo del secondo,
- C — tipo del risultato combinato.
Esempio
CompletableFuture<String> nameFuture = CompletableFuture.supplyAsync(() -> loadUserName());
CompletableFuture<Integer> balanceFuture = CompletableFuture.supplyAsync(() -> loadUserBalance());
CompletableFuture<String> resultFuture = nameFuture.thenCombine(
balanceFuture,
(name, balance) -> "Nome: " + name + ", Saldo: " + balance
);
resultFuture.thenAccept(System.out::println);
Come funziona:
- Entrambi i future partono in parallelo.
- Non appena entrambi sono terminati, viene invocata la funzione (name, balance) -> ....
- Il future risultante contiene la stringa con il risultato.
Mini esempio con i numeri
CompletableFuture<Integer> f1 = CompletableFuture.supplyAsync(() -> 2);
CompletableFuture<Integer> f2 = CompletableFuture.supplyAsync(() -> 3);
CompletableFuture<Integer> sum = f1.thenCombine(f2, Integer::sum);
sum.thenAccept(result -> System.out.println("Somma: " + result));
Output:
Somma: 5
Variante asincrona
Se la combinazione è un’operazione pesante, usa thenCombineAsync:
f1.thenCombineAsync(f2, (a, b) -> a * b);
3. allOf: quando le attività sono molte
E se non avessimo due attività, ma una decina? Per esempio, vogliamo caricare in parallelo i dati di dieci utenti. Per questo esiste il metodo CompletableFuture.allOf.
Descrizione
CompletableFuture.allOf(f1, f2, ..., fn) restituisce un nuovo future che si completa quando si completano tutte le attività passate. Ma c’è una sfumatura: questo future non contiene alcun risultato — il suo tipo è sempre CompletableFuture<Void>. Per ottenere i risultati, bisogna separatamente «recuperarli» dai future originali.
Esempio
CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> "Primo");
CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> "Secondo");
CompletableFuture<Void> all = CompletableFuture.allOf(f1, f2);
all.thenRun(() -> {
// Tutte le attività sono terminate!
String s1 = f1.join(); // join() — come get(), ma lancia un'eccezione unchecked
String s2 = f2.join();
System.out.println(s1 + " & " + s2);
});
Output:
Primo & Secondo
Esempio con un array di attività
List<CompletableFuture<String>> futures = new ArrayList<>();
for (int i = 0; i < 5; i++) {
int id = i;
futures.add(CompletableFuture.supplyAsync(() -> "Utente " + id));
}
CompletableFuture<Void> all = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
all.thenRun(() -> {
for (CompletableFuture<String> f : futures) {
System.out.println(f.join());
}
});
Cosa succede:
- Tutte le attività vengono avviate in parallelo.
- allOf si completerà quando tutte le attività saranno completate.
- Nel blocco thenRun possiamo ottenere i risultati tramite join().
Schema visivo
[Future1] \
[Future2] ----> [allOf] ---> thenRun
[Future3] /
4. anyOf: attendiamo la prima attività che si completa
A volte non serve aspettare tutti, ma ottenere il risultato dell’attività più veloce. Ad esempio, chiediamo i dati a due server — useremo quelli di chi risponde per primo. Per questo esiste CompletableFuture.anyOf.
Descrizione
CompletableFuture.anyOf(f1, f2, ..., fn) restituisce un future che si completa quando si completa una qualsiasi delle attività passate. Il tipo del risultato è CompletableFuture<Object>, perché i tipi delle attività possono essere diversi.
Esempio
CompletableFuture<String> fast = CompletableFuture.supplyAsync(() -> {
sleep(500);
return "Server veloce";
});
CompletableFuture<String> slow = CompletableFuture.supplyAsync(() -> {
sleep(2000);
return "Server lento";
});
CompletableFuture<Object> any = CompletableFuture.anyOf(fast, slow);
any.thenAccept(result -> System.out.println("Ricevuto: " + result));
Output:
Ricevuto: Server veloce
Esempio con tipi diversi
Si possono combinare attività di tipi diversi, ma in tal caso il risultato sarà di tipo Object, e sarà necessario un cast esplicito.
5. Dettagli utili
Raccomandazioni
- allOf non restituisce un array di risultati. Occorre mantenere i future originali per recuperarne i valori tramite join() o get().
- anyOf restituisce il primo risultato completato, ma di tipo Object. Se tutte le attività sono dello stesso tipo, si può effettuare un cast.
- Se una delle attività in allOf termina con errore — il future risultante terminerà anch’esso con errore.
- Per thenCombine entrambe le attività devono terminare con successo, altrimenti — eccezione.
Tabella di confronto dei metodi
| Metodo | Quando usarlo | Tipo di risultato |
|---|---|---|
|
Devi unire i risultati di due attività | Risultato della combinazione |
|
Devi attendere il completamento di tutte le attività | |
|
Devi attendere il completamento di una qualsiasi attività | Object (risultato della prima attività) |
6. Errori tipici nella combinazione di CompletableFuture
Errore n. 1: Attendere il risultato tramite get()/join() nel thread principale.
Se scrivi codice asincrono ma alla fine chiami comunque get() o join(), blocchi il thread e perdi tutti i vantaggi dell’asincronia. È meglio usare thenAccept/thenRun per elaborare il risultato senza bloccare.
Errore n. 2: Non conservare i riferimenti ai future originali quando si usa allOf.
Se hai chiamato CompletableFuture.allOf(f1, f2, f3), ma non hai salvato f1, f2, f3 — non potrai ottenerne i risultati. allOf restituisce solo Void!
Errore n. 3: Non gestire gli errori nella catena.
Se una delle attività termina con errore, l’intero allOf o thenCombine terminerà anch’esso con errore. Usa i metodi di gestione degli errori (exceptionally, handle, whenComplete) per non perdere le eccezioni.
Errore n. 4: Incompatibilità di tipi con anyOf.
anyOf restituisce Object. Se i tuoi future restituiscono tipi diversi, dovrai capire cosa è arrivato per primo. Meglio usare tipi omogenei, se possibile.
Errore n. 5: Catene troppo complesse senza commenti.
Quando il codice cresce, le catene di future possono trasformarsi in «spaghetti». Non esitare a spezzare la catena in variabili separate e a commentare i passaggi.
GO TO FULL VERSION