CodeGym /Corsi /JAVA 25 SELF /Introduzione a CompletableFuture

Introduzione a CompletableFuture

JAVA 25 SELF
Livello 55 , Lezione 0
Disponibile

1. Il problema del codice sincrono

Immaginiamo: avete un programma che deve scaricare dati da Internet o leggere un file grande. Scrivete qualcosa del genere:

String data = readFromFile("bigfile.txt");
System.out.println("Dati: " + data);

Tutto bene, ma se il file è grande o la rete è lenta, il programma semplicemente si blocca sulla riga di lettura. L’utente vede un’interfaccia “bloccata”, il server non può servire altre richieste e lo sviluppatore... si rattrista.

Questa situazione si chiama blocco: un thread (ad esempio, il thread principale della vostra applicazione) è costretto ad aspettare finché l’operazione non termina. E se tali operazioni sono molte — ecco che arrivano lag e prestazioni scarse.

È come andare al bar, fare l’ordine e... dover restare al bancone finché non vi preparano il caffè. Gli altri clienti restano in coda e aspettano anche loro finché il barista non finisce con voi. Inefficiente, vero?

Asincronia: come salva il mondo

La programmazione asincrona è un approccio in cui le operazioni lunghe (ad esempio, lettura di un file, richiesta a un server, accesso a un database) vengono eseguite in un thread in background, mentre il thread principale continua a lavorare: servire utenti, accettare nuove richieste, reagire agli eventi.

Cioè, fate l’ordine (avviate un’attività), andate a fare altro e, quando il caffè è pronto (l’attività è terminata), vi dicono semplicemente: “Pronto!”

In Java, prima dell’arrivo di CompletableFuture, non era molto comodo. Vediamo come si è evoluto.

2. Approcci storici: Future e i suoi limiti

In Java 5 è stata introdotta l’interfaccia Future — il primo tentativo di rendere il lavoro con attività asincrone almeno un po’ più comodo. Permetteva di affidare un’attività a un thread pool e ottenere prima o poi il risultato.

ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Integer> future = executor.submit(() -> 2 + 2);
int result = future.get(); // Attenzione: il thread si bloccherà finché l'attività non sarà completata!

L’idea sembra buona, ma in pratica Future è come una vecchia cassetta della posta: inviate la lettera, ma per sapere se è arrivata una risposta dovete continuare a guardare dentro.

Non sa notificare quando il risultato è pronto, non supporta catene di azioni come “fai questo e poi quello”, non consente di gestire gli errori in modo elegante. Tutto si riduce alla chiamata bloccante get(), a causa della quale l’asincronia torna a essere attesa.

3. La nascita di CompletableFuture: un nuovo stile di asincronia

In Java 8, al posto del vecchio Future, è arrivato il vero eroe dell’asincronia — CompletableFuture. Questa classe del package java.util.concurrent è diventata uno strumento universale per chi era stanco di aspettare i risultati “a mano” e voleva scrivere codice asincrono in modo elegante, compatto e chiaro.

CompletableFuture sa fare quasi tutto. Può avviare attività in altri thread, metterle in catena — per esempio, prima calcolare un risultato, poi elaborarlo e infine fare qualcos’altro. Combina facilmente più attività: si può attendere il completamento di tutte oppure solo della prima che finisce. Anche gli errori si gestiscono in modo elegante — senza troppi try-catch. E lo stile complessivo diventa più funzionale: al posto di chiamate noiose e attese compaiono metodi espressivi come thenApply, thenAccept e altri.

flowchart LR
    A[Avvio dell'attività in modo asincrono] --> B[Elaborazione del risultato]
    B --> C[Operazione successiva]
    C --> D[Gestione degli errori]

Così CompletableFuture ha trasformato l’asincronia da mestiere pesante in uno strumento comodo e flessibile, con cui il codice finalmente respira.

4. Un esempio semplicissimo: il primo passo nel mondo di CompletableFuture

Vediamo un esempio minimo di attività asincrona:

import java.util.concurrent.CompletableFuture;

public class AsyncDemo {
    public static void main(String[] args) {
        // Avviamo l'attività in modo asincrono
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> 2 + 2);

        // Otteniamo il risultato (blocca il thread!)
        try {
            int result = future.get();
            System.out.println("Risultato: " + result); // 4
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Questo codice esegue già il calcolo in un thread separato — il thread principale non si blocca al momento dell’avvio dell’attività. Ma la chiamata get() blocca comunque il thread finché il risultato non è pronto.

E come NON bloccare il thread?

È semplice: usate i metodi callback, che vengono chiamati quando l’attività termina:

CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> 2 + 2);

future.thenAccept(result -> System.out.println("Risultato: " + result));
System.out.println("Non mi blocco e posso fare qualcos'altro!");

Conclusione:

  • thenAccept — è “iscriviti al risultato”: quando l’attività è completata, esegui questo codice.
  • Il thread principale non aspetta il completamento dell’attività e continua a lavorare.

Visualizzazione (pseudo-codice degli eventi)

[Thread principale] --> [Avvio dell'attività]
     |                   |
     v                   v
[Fa qualcos'altro]   [Il thread in background calcola 2+2]
     |                   |
     v                   v
[Stampa "Non mi blocco..."]
     |                   |
     v                   v
                 [Quando è calcolato — viene chiamato thenAccept]

5. Come appare in un’applicazione?

Immaginiamo che stiate sviluppando un’app console, in cui l’utente può richiedere il caricamento dei dati (per esempio da un database o da un server) e, mentre i dati si caricano, il programma non si “blocca” ma continua ad accettare comandi.

Esempio: simulazione di un’operazione lunga

import java.util.concurrent.CompletableFuture;

public class AsyncApp {
    public static void main(String[] args) {
        System.out.println("Iniziamo a caricare i dati...");

        CompletableFuture<String> dataFuture = CompletableFuture.supplyAsync(() -> {
            // Simulazione di un caricamento lungo
            try {
                Thread.sleep(2000); // 2 secondi
            } catch (InterruptedException e) {
                return "Errore di caricamento";
            }
            return "Dati caricati con successo!";
        });

        // Ci registriamo al risultato
        dataFuture.thenAccept(result -> System.out.println("Risultato: " + result));

        // Il programma continua a lavorare
        System.out.println("Mentre i dati si caricano, posso fare qualcos'altro!");

        // Per evitare che il programma termini troppo presto (solo per la demo!)
        try {
            Thread.sleep(2500);
        } catch (InterruptedException ignored) {}
    }
}

Cosa vedrete in console:

Iniziamo a caricare i dati...
Mentre i dati si caricano, posso fare qualcos'altro!
[dopo 2 secondi]
Risultato: Dati caricati con successo!

6. Dettagli utili

Un po’ di thread sotto il cofano

Quando scrivete CompletableFuture.supplyAsync(...), l’attività per impostazione predefinita viene eseguita nel cosiddetto ForkJoinPool — uno speciale thread pool che Java usa per attività parallele. Se vi serve più controllo (ad esempio, un vostro ExecutorService), potete passarlo come secondo parametro:

ExecutorService executor = Executors.newFixedThreadPool(2);
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> 2 + 2, executor);

Ma per attività semplici basta il pool standard.

Ottenere il risultato: get(), join(), thenAccept

  • get() — blocca il thread finché il risultato non è pronto (lancia eccezioni checked).
  • join() — blocca anch’esso, ma lancia eccezioni unchecked (RuntimeException).
  • thenAccept(), thenApply() e altri — NON bloccano, ma invocano la funzione passata quando il risultato è pronto.

Nelle applicazioni asincrone reali cercate di evitare get()/join() nel thread principale!

7. Errori tipici ai primi passi con CompletableFuture

Errore n. 1: uso di get() o join() nel thread principale.
Così bloccate di nuovo il programma e perdete tutti i vantaggi dell’asincronia. Invece, usate thenAccept, thenApply e altri metodi per l’elaborazione del risultato.

Errore n. 2: dimenticare di gestire l’errore.
Se in un’attività asincrona si verifica un’eccezione, non “salterà fuori” nel thread principale. Senza gestione tramite exceptionally o handle semplicemente non saprete che qualcosa è andato storto.

Errore n. 3: non avete atteso la fine del programma.
Nei demo spesso bisogna “rallentare” il thread main con Thread.sleep — altrimenti il programma termina prima che l’attività finisca. Nelle applicazioni reali (ad esempio nei server web) non è un problema, ma nelle demo da console tenetelo a mente.

Errore n. 4: confondere thenAccept e thenApply.
thenAccept è per “effetti collaterali” (non ritorna nulla), thenApply è per trasformare il risultato (ritorna un nuovo risultato).

Errore n. 5: mescolare codice asincrono e sincrono senza necessità.
Se avete iniziato a scrivere in modo asincrono, non riportate tutto nel sincrono con get()/join(), se non in casi estremi (ad esempio, nei test).

1
Compito
JAVA 25 SELF, livello 55, lezione 0
Bloccato
Il Cuore del Pensatore: Trasformazione Asincrona dell'Energia
Il Cuore del Pensatore: Trasformazione Asincrona dell'Energia
1
Compito
JAVA 25 SELF, livello 55, lezione 0
Bloccato
Guardiano Finanziario: Gestione dei Rischi nelle Transazioni Asincrone
Guardiano Finanziario: Gestione dei Rischi nelle Transazioni Asincrone
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION