Future e Callable in Android Java: Guida Completa

Future Callable Android Java - illustrazione concetti di concorrenza in Android

Perché Callable supera Runnable in Android (e come usarlo con Future)

In questa guida analizziamo in profondità Future Callable Android Java, concentrandoci in particolare su Callable e Future e sul loro utilizzo nelle app Android. Vedremo differenze con Runnable, integrazione con ExecutorService, gestione delle eccezioni, FutureTask, confronto con ListenableFuture (Guava), rapporto con AsyncTask (deprecato) e RxJava, oltre a best practice e pitfall comuni.

1. Introduzione a Callable e Future in Android Java (vs Runnable)

Runnable e Callable sono interfacce funzionali usate per rappresentare unità di lavoro eseguibili in thread separati o tramite pool di thread.

Runnable:

// Runnable: nessun valore di ritorno, nessuna eccezione dichiarata
Runnable runnable = new Runnable() {
    @Override
    public void run() {
        // Esegui un compito in background
        someBackgroundWork();
    }
};

Callable<V>:

// Callable: ritorna un valore di tipo V e può lanciare eccezioni checked
Callable<Integer> callable = new Callable<Integer>() {
    @Override
    public Integer call() throws Exception {
        // Esegui logica complessa
        int result = computeSomething();
        return result; // Il valore sarà recuperabile tramite un Future
    }
};

Le differenze principali tra Runnable e Callable:

  • Runnable.run() non restituisce alcun valore (ritorno void).
  • Callable.call() restituisce un valore (V).
  • Callable.call() può dichiarare e lanciare eccezioni checked.

Future<V> rappresenta il risultato futuro di un calcolo asincrono. Viene tipicamente restituito quando si sottomette un Callable (o un Runnable con risultato) a un ExecutorService. Tramite un Future è possibile:

  • Recuperare il risultato del calcolo (bloccando il thread chiamante se necessario).
  • Verificare se il calcolo è terminato.
  • Annullare il calcolo se ancora in corso.

2. Come usare Callable e Future in Android Java con ExecutorService per ottenere un Future

La modalità più comune per usare Callable in Android/Java è tramite un ExecutorService, ovvero un pool di thread gestito che esegue task asincroni per noi.

// 1. Creazione di un ExecutorService (per es., un pool fisso di 3 thread)
ExecutorService executor = Executors.newFixedThreadPool(3);
// 2. Definizione di un Callable che restituisce un valore
Callable<String> downloadTask = new Callable<String>() {
    @Override
    public String call() throws Exception {
        // Simula un lavoro pesante (ad esempio, una richiesta di rete)
        Thread.sleep(2000L);
        return "Risultato del download";
    }
};
// 3. Sottomissione del Callable e ottenimento di un Future
Future<String> future = executor.submit(downloadTask);
// 4. Più tardi, da un thread in background, possiamo ottenere il risultato
try {
    String result = future.get(); // Bloccante finché il task non termina
    // Usa il risultato (non dal main thread se è un lavoro pesante)
} catch (ExecutionException e) {
    // Il task ha lanciato un'eccezione
} catch (InterruptedException e) {
    // Il thread chiamante è stato interrotto mentre aspettava
}

In Android, è importante non chiamare metodi bloccanti come future.get() sul thread principale, per evitare ANR (Application Not Responding).

3. Metodi principali di Future in Android Java: get(), cancel(), isDone(), isCancelled()

L’interfaccia Future<V> espone diversi metodi chiave. I più usati sono:

  • V get() e V get(long timeout, TimeUnit unit)
  • boolean cancel(boolean mayInterruptIfRunning)
  • boolean isDone()
  • boolean isCancelled()
ExecutorService executor = Executors.newSingleThreadExecutor();
Callable<Integer> longComputation = new Callable<Integer>() {
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 0; i < 10; i++) {
            if (Thread.currentThread().isInterrupted()) {
                // Pulizia e uscita precoce se il thread è stato interrotto
                throw new InterruptedException("Task interrotto");
            }
            Thread.sleep(500L); // Simula lavoro
            sum += i;
        }
        return sum;
    }
};
Future<Integer> future = executor.submit(longComputation);
// Esempio di uso dei metodi principali
try {
    // Attendi al massimo 3 secondi per il risultato
    Integer result = future.get(3, TimeUnit.SECONDS);
} catch (TimeoutException e) {
    // Il task sta impiegando troppo tempo, proviamo ad annullarlo
    boolean cancelled = future.cancel(true); // true = prova a interrompere il thread
} catch (ExecutionException e) {
    // Eccezione lanciata dal call()
} catch (InterruptedException e) {
    // Il thread chiamante è stato interrotto
}
// Controlli di stato
boolean done = future.isDone();          // true se il task è terminato (con successo, errore o cancellazione)
boolean wasCancelled = future.isCancelled(); // true se il task è stato annullato

Uso tipico in Android: controllare isDone() / isCancelled() solo da thread in background o da componenti che gestiscono lo stato (es. ViewModel), evitando blocchi sul main thread.

4. Gestione delle eccezioni nei task asincroni

Quando si invoca future.get(), possono verificarsi principalmente due eccezioni checked:

  • ExecutionException: incapsula l’eccezione lanciata da Callable.call().
  • InterruptedException: indica che il thread che stava aspettando il risultato è stato interrotto.
Callable<String> riskyTask = new Callable<String>() {
    @Override
    public String call() throws Exception {
        if (Math.random() < 0.5) {
            throw new IOException("Errore di IO simulato");
        }
        return "OK";
    }
};
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(riskyTask);
try {
    String value = future.get();
} catch (ExecutionException e) {
    // L'eccezione reale è la causa
    Throwable cause = e.getCause();
    if (cause instanceof IOException) {
        // Gestione specifica per IOException
    } else {
        // Gestione generica
    }
} catch (InterruptedException e) {
    // Ripristinare lo stato di interruzione è una best practice
    Thread.currentThread().interrupt();
    // Eventuale log o logica di uscita
}

In Android, la corretta gestione di InterruptedException è fondamentale per evitare thread “zombie” e per cooperare con le cancellazioni (ad esempio, quando un ViewModel o un componente viene distrutto).

5. FutureTask: un approccio alternativo

FutureTask<V> è una classe che implementa sia Runnable sia Future<V>. Può essere vista come un “contenitore” eseguibile di un Callable o Runnable che produce un risultato recuperabile in seguito.

  • Può essere passata direttamente a un Thread o a un ExecutorService.
  • Permette un controllo più diretto rispetto al solo Future restituito da submit().
Callable<String> callable = new Callable<String>() {
    @Override
    public String call() throws Exception {
        Thread.sleep(1000L);
        return "Risultato da FutureTask";
    }
};
// 1. Creazione di una FutureTask
FutureTask<String> futureTask = new FutureTask<>(callable);
// 2a. Esecuzione tramite un thread dedicato
Thread thread = new Thread(futureTask);
thread.start();
// 2b. Oppure tramite un ExecutorService:
// executor.execute(futureTask);
// 3. Recupero del risultato
try {
    String result = futureTask.get();
} catch (ExecutionException | InterruptedException e) {
    // Gestione eccezioni
}

FutureTask è utile quando si vuole avere un unico oggetto che rappresenti sia il task eseguibile sia il suo risultato futuro, ad esempio per riutilizzo o per collegarlo a più componenti.

6. Esempio pratico completo

Supponiamo di avere un’Activity che deve eseguire una chiamata di rete (simulata) in background e aggiornare la UI con il risultato.

public class MainActivity extends AppCompatActivity {
    private ExecutorService executorService;
    private TextView resultTextView;
    private Future<String> currentRequest;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        resultTextView = findViewById(R.id.text_result);
        // Pool di thread per richieste di rete
        executorService = Executors.newFixedThreadPool(3);
        findViewById(R.id.button_start).setOnClickListener(v -> startFakeNetworkCall());
    }
    private void startFakeNetworkCall() {
        // Evita di lanciare più richieste contemporaneamente
        if (currentRequest != null && !currentRequest.isDone()) {
            return;
        }
        // Definisci il Callable che simula una chiamata di rete
        Callable<String> networkCallable = new Callable<String>() {
            @Override
            public String call() throws Exception {
                // Simula latenza di rete
                Thread.sleep(2000L);
                // Simula possibile errore
                if (Math.random() < 0.3) {
                    throw new IOException("Errore di rete simulato");
                }
                return "Dati ricevuti dal server";
            }
        };
        // Sottometti il task e ottieni un Future
        currentRequest = executorService.submit(networkCallable);
        // Gestisci il risultato in un altro thread del pool
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    // Questa chiamata è bloccante ma è su un thread in background
                    final String result = currentRequest.get();
                    // Aggiorna la UI sul main thread
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            resultTextView.setText(result);
                        }
                    });
                } catch (ExecutionException e) {
                    final Throwable cause = e.getCause();
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            resultTextView.setText("Errore: " + cause.getMessage());
                        }
                    });
                } catch (InterruptedException e) {
                    // Ripristina lo stato di interruzione
                    Thread.currentThread().interrupt();
                }
            }
        });
    }
    @Override
    protected void onDestroy() {
        super.onDestroy();
        // Annulla eventuale richiesta in corso per evitare leak o crash
        if (currentRequest != null && !currentRequest.isDone()) {
            currentRequest.cancel(true);
        }
        // Spegni il pool di thread
        executorService.shutdownNow();
    }
}

Punti chiave:

  • La chiamata pesante (currentRequest.get()) avviene sempre fuori dal main thread.
  • L’aggiornamento della UI è incapsulato in runOnUiThread(), quindi eseguito sul thread principale.
  • In onDestroy() il task viene annullato e il pool chiuso per evitare memory leak.

7. Differenze tra Future, FutureTask e ListenableFuture (Guava) in Android Java

Future<V> (Java standard):

  • Interfaccia di base.
  • Permette di recuperare lo stato/risultato, ma non offre callback nativi.
  • Tipicamente usato con ExecutorService.submit().

FutureTask<V> (Java standard):

  • Implementa sia Runnable sia Future<V>.
  • Utile quando vuoi un oggetto unico che rappresenti il task e il suo risultato.
  • Può essere passato a Thread o ad un ExecutorService.

ListenableFuture<V> (Guava):

  • Estende il concetto di Future aggiungendo callback che vengono eseguiti al completamento del task.
  • Evita la necessità di chiamare get() in blocco; invece, registri un listener.
  • Molto utile in contesti reattivi o quando si vogliono comporre più operazioni asincrone.

Per usare ListenableFuture, aggiungi Guava al file build.gradle del modulo app:

dependencies {
    implementation 'com.google.guava:guava:33.2.0-android'
}
// Esempio concettuale con ListenableFuture (Guava)
ListeningExecutorService service = MoreExecutors.listeningDecorator(
        Executors.newFixedThreadPool(3)
);
ListenableFuture<String> listenableFuture = service.submit(new Callable<String>() {
    @Override
    public String call() throws Exception {
        Thread.sleep(1000L);
        return "Risultato";
    }
});
Futures.addCallback(listenableFuture, new FutureCallback<String>() {
    @Override
    public void onSuccess(final String result) {
        // Esegui qualcosa quando il risultato è pronto
    }
    @Override
    public void onFailure(Throwable t) {
        // Gestione errore
    }
}, MoreExecutors.directExecutor()); // O un executor specifico per la UI
FutureFutureTaskListenableFuture (Guava)
Callback nativi
Composizione async✅ (con Futures.*)
Dipendenze esterneNessunaNessunaGuava
Può fare da Runnable
Uso consigliatoTask singolo sempliceTask con controllo direttoOperazioni componibili

In Android moderno, una funzionalità simile è offerta da API come LiveData, Flow, CompletableFuture (da Java 8+ su alcune piattaforme) o librerie reattive come RxJava.

8. Confronto tra le soluzioni asincrone in Android

AsyncTask è stato a lungo una soluzione semplice per compiti asincroni in Android, ma ora è deprecato a causa di vari problemi:

  • Gestione non ottimale del lifecycle (rischio memory leak).
  • API rigida e difficile da comporre.
  • Scarsa separazione tra logica di business e UI.

Future + ExecutorService è adatto quando:

  • Hai compiti indipendenti e relativamente semplici.
  • Ti basta eseguire in background e leggere il risultato (magari aggiornando la UI con runOnUiThread()).
  • Vuoi evitare dipendenze da librerie esterne.

RxJava è consigliato quando:

  • Devi comporre più operazioni asincrone (catene di trasformazioni, combinazioni, filtri).
  • Hai bisogno di stream di valori (non solo un singolo risultato).
  • Vuoi un controllo granulare sui thread (scheduler) e strumenti avanzati (retry, backpressure, ecc.).

In un progetto Android moderno, spesso si preferiscono ancora altre alternative:

Kotlin Coroutines: la scelta preferita in Kotlin

Se il tuo progetto è in Kotlin, le Coroutines sono oggi la soluzione raccomandata da Google per l’asincronia Android. Rispetto a Future, non bloccano il thread chiamante e si integrano nativamente con ViewModel e Lifecycle.

// Equivalente Kotlin Coroutines del pattern Callable + Future
viewModelScope.launch {
    val result = withContext(Dispatchers.IO) {
        // Lavoro in background (equivalente a Callable.call())
        fetchDataFromNetwork() // restituisce String
    }
    // Qui siamo di nuovo sul Main thread
    _uiState.value = result
}

withContext(Dispatchers.IO) è il corrispettivo diretto del Callable: esegue il blocco su un thread pool dedicato all’I/O e restituisce il risultato al chiamante — senza mai bloccare il Main thread e senza la cerimonia di ExecutorService, submit() e get().

  • Kotlin Coroutines (se il progetto è in Kotlin), che sostituiscono in gran parte l’uso diretto di Future e RxJava.
  • Worker / WorkManager per lavori differiti e persistenti.
  • LiveData o Flow per l’osservazione dei risultati sulla UI.

9. Best practice e pitfall comuni

Best practice:

  • Non bloccare mai il main thread con future.get() o operazioni lunghe.
  • Chiudi sempre gli ExecutorService quando non sono più necessari (shutdown() / shutdownNow()).
  • Gestisci correttamente InterruptedException, propagando o ripristinando lo stato con Thread.currentThread().interrupt().
  • Usa pool di thread ragionevoli: troppi thread possono peggiorare le performance.
  • Nel codice Android, collega la vita dei task al lifecycle dei componenti (Activity, Fragment, ViewModel).

Pitfall comuni:

  • Chiamare future.get() dal main thread, causando freeze dell’UI o ANR.
  • Dimenticare di annullare task e chiudere pool di thread, causando memory leak.
  • Ignorare ExecutionException senza analizzare getCause(), perdendo il dettaglio dell’errore.
  • Usare new Thread(...) in modo indiscriminato invece di un ExecutorService, rendendo difficile la gestione globale delle risorse.
  • Condividere un ExecutorService tra componenti con lifecycle molto diversi senza adeguate cautele.

Metti tutto insieme: mini app completa con Callable e Future in Android Java

Vediamo ora una mini app completa che simula tre chiamate API in parallelo (meteo, notizie, profilo utente). Le eseguiamo in background con un pool di thread, raccogliamo i risultati con invokeAll(), aggiorniamo la UI con runOnUiThread() e ci assicuriamo di ripulire tutto in onDestroy().

public class MainActivity extends AppCompatActivity {
    private ExecutorService executorService;
    private TextView tvWeather;
    private TextView tvNews;
    private TextView tvProfile;
    private Button btnLoad;
    private boolean isLoading = false;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // Inizializza la UI
        tvWeather = findViewById(R.id.tvWeather);
        tvNews = findViewById(R.id.tvNews);
        tvProfile = findViewById(R.id.tvProfile);
        btnLoad = findViewById(R.id.btnLoad);
        // Pool fisso di 3 thread per eseguire le chiamate in parallelo
        executorService = Executors.newFixedThreadPool(3);
        btnLoad.setOnClickListener(v -> {
            if (isLoading) {
                // Evita doppi click mentre le chiamate sono in corso
                return;
            }
            isLoading = true;
            tvWeather.setText("Caricamento meteo...");
            tvNews.setText("Caricamento notizie...");
            tvProfile.setText("Caricamento profilo...");
            startParallelApiCalls();
        });
    }
    private void startParallelApiCalls() {
        // Spostiamo il lavoro bloccante (invokeAll) fuori dal main thread
        executorService.execute(() -> {
            List<Callable<String>> tasks = Arrays.asList(
                    // Simula una chiamata API meteo
                    () -> fetchWeather(),
                    // Simula una chiamata API notizie
                    () -> fetchNews(),
                    // Simula una chiamata API profilo utente
                    () -> fetchUserProfile()
            );
            try {
                // Esegue tutti i task in parallelo e attende che terminino
                List<Future<String>> futures = executorService.invokeAll(tasks);
                // Estrae i risultati, gestendo le eccezioni per ogni singolo Future
                String weatherResult = "";
                String newsResult = "";
                String profileResult = "";
                for (int i = 0; i < futures.size(); i++) {
                    Future<String> future = futures.get(i);
                    try {
                        String value = future.get();
                        if (i == 0) weatherResult = value;
                        else if (i == 1) newsResult = value;
                        else if (i == 2) profileResult = value;
                    } catch (ExecutionException e) {
                        // Una singola chiamata può fallire senza bloccare le altre
                        String error = "Errore: " + e.getCause().getMessage();
                        if (i == 0) weatherResult = error;
                        else if (i == 1) newsResult = error;
                        else if (i == 2) profileResult = error;
                    }
                }
                // Aggiorna la UI sul main thread
                String finalWeatherResult = weatherResult;
                String finalNewsResult = newsResult;
                String finalProfileResult = profileResult;
                runOnUiThread(() -> {
                    tvWeather.setText(finalWeatherResult);
                    tvNews.setText(finalNewsResult);
                    tvProfile.setText(finalProfileResult);
                    isLoading = false;
                });
            } catch (InterruptedException e) {
                // Se il thread viene interrotto, ripristina il flag e termina
                Thread.currentThread().interrupt();
                runOnUiThread(() -> isLoading = false);
            }
        });
    }
    // Metodi che simulano le chiamate API con latenze diverse
    private String fetchWeather() throws Exception {
        Thread.sleep(800L); // 800 ms
        return "Meteo: soleggiato, 24°C";
    }
    private String fetchNews() throws Exception {
        Thread.sleep(1200L); // 1200 ms
        // Simula la possibilità di errore
        if (Math.random() < 0.2) {
            throw new IOException("Impossibile caricare le notizie");
        }
        return "Notizie: 3 nuovi articoli disponibili";
    }
    private String fetchUserProfile() throws Exception {
        Thread.sleep(600L); // 600 ms
        return "Profilo: ciao, Martina!";
    }
    @Override
    protected void onDestroy() {
        super.onDestroy();
        // Chiude il pool di thread per evitare leak quando l'Activity viene distrutta
        if (executorService != null && !executorService.isShutdown()) {
            executorService.shutdownNow();
        }
    }
}

Punti chiave architetturali:

  • invokeAll() esegue i tre Callable in parallelo e ci restituisce una lista di Future omogenei, semplificando la sincronizzazione rispetto a tre submit() separati.
  • Il blocco su invokeAll() avviene sempre in un thread del pool, mai sul main thread, mantenendo reattiva la UI.
  • Il try/catch attorno a ogni future.get() ci permette di gestire gli errori delle singole chiamate senza far fallire l’intero flusso.
  • runOnUiThread() incapsula l’aggiornamento dei tre TextView, garantendo che tutte le modifiche alla UI avvengano sul thread principale.
  • Il booleano isLoading protegge da doppi click e race condition lato utente, mentre onDestroy() chiude il pool per allineare il ciclo di vita dei thread a quello dell’Activity.

Domande frequenti

Qual è la differenza tra Callable e Runnable in Android Java? Runnable non restituisce alcun valore e non può lanciare eccezioni checked, mentre Callable in Android Java restituisce un risultato tipizzato e può dichiarare eccezioni checked. Per task che devono restituire dati (ad esempio risposta di rete o risultato di una query), Callable è la scelta corretta.

Quando usare Future rispetto ad altre soluzioni asincrone in Android? Future è ideale quando ti serve un solo risultato asincrono, vuoi evitare dipendenze esterne e ti basta bloccare o controllare lo stato del task da un thread in background. Se ti servono stream continui di dati o catene complesse di operazioni, è meglio valutare RxJava, Kotlin Coroutines o Flow.

Come evitare di bloccare il main thread con Future in Android? Non chiamare mai future.get() dal main thread. Esegui get() solo dentro un thread di background o un pool di thread (ExecutorService) e poi propaga il risultato alla UI usando runOnUiThread(), ViewModel + LiveData/Flow o altri meccanismi sicuri per il thread principale.

Qual è l’alternativa moderna a Callable e Future in Android Kotlin? In progetti Kotlin, l’alternativa raccomandata a Callable e Future è usare le Kotlin Coroutines con Dispatchers.IO per il lavoro in background e viewModelScope per legare le coroutine al lifecycle. Questo approccio evita blocchi, si integra con LiveData/Flow ed è meglio supportato dalle librerie Jetpack.

10. Conclusione

Callable e Future forniscono fondamenta robuste per l’esecuzione asincrona in Java e Android. Rispetto a Runnable, Callable consente di restituire valori e propagare eccezioni checked, mentre Future permette di controllare, annullare e ottenere i risultati dei task eseguiti da un ExecutorService (come visto in come usare Callable e Future in Android Java con ExecutorService). FutureTask offre un wrapper potente che unisce comportamento eseguibile e gestione del risultato (approfondito in FutureTask con Callable e Future in Android Java), mentre soluzioni come ListenableFuture (Guava) e librerie moderne come RxJava e Kotlin Coroutines aggiungono capacità di composizione e reattività.

Per uno sviluppatore Android intermedio, padroneggiare Future e Callable in Android Java significa comprendere a fondo il modello di concorrenza di base su cui si appoggiano molte astrazioni più evolute (riassunte nelle best practice su Callable e Future in Android Java). Anche se in progetti moderni potresti preferire coroutines o RxJava, conoscere queste API ti aiuterà a scrivere codice più sicuro, efficiente e manutenibile, specialmente quando lavori con librerie Java legacy o quando devi mantenere un controllo fine sui thread sottostanti. In caso di dubbi rapidi, puoi anche consultare la sezione FAQ su Callable e Future in Android Java.

Commenti

Rispondi

Scopri di più da App Tutorial

Abbonati ora per continuare a leggere e avere accesso all'archivio completo.

Continua a leggere