Panoramica
Fin dai primi giorni di Java, il multithreading è stato un aspetto importante del linguaggio. Runnable è l’interfaccia principale fornita per rappresentare attività multi-thread e Callable è una versione migliorata di Runnable che è stata aggiunta in Java 1.5.
In questo articolo, esploreremo le differenze e le applicazioni di entrambe le interfacce.
Meccanismo di esecuzione
Entrambe le interfacce sono progettate per rappresentare un’attività che può essere eseguita da più thread. Le attività eseguibili possono essere eseguite utilizzando la classe Thread o ExecutorService mentre i Callable possono essere eseguiti solo utilizzando quest’ultimo.
Valori di ritorno
Diamo uno sguardo più profondo al modo in cui queste interfacce gestiscono i valori di ritorno.
3.1. Con Runnable
L’interfaccia Runnable è un’interfaccia funzionale e ha un singolo metodo run() che non accetta alcun parametro e non restituisce alcun valore.
Questo è adatto per situazioni in cui non stiamo cercando un risultato dell’esecuzione del thread, ad esempio, la registrazione degli eventi in entrata:
public interface Runnable { public void run();}
Capiamo questo con un esempio:
public class EventLoggingTask implements Runnable{ private Logger logger = LoggerFactory.getLogger(EventLoggingTask.class); @Override public void run() { logger.info("Message"); }}
In questo esempio, il thread leggerà semplicemente un messaggio dalla coda e lo registrerà in un file di registro. Non viene restituito alcun valore dall’attività; l’attività può essere avviata utilizzando ExecutorService:
public void executeTask() { executorService = Executors.newSingleThreadExecutor(); Future future = executorService.submit(new EventLoggingTask()); executorService.shutdown();}
In questo caso, l’oggetto Futuro non manterrà alcun valore.
3.2. Con Callable
L’interfaccia Callable è un’interfaccia generica contenente un metodo single call () – che restituisce un valore generico V:
public interface Callable<V> { V call() throws Exception;}
Diamo un’occhiata al calcolo del fattoriale di un numero:
public class FactorialTask implements Callable<Integer> { int number; // standard constructors public Integer call() throws InvalidParamaterException { int fact = 1; // ... for(int count = number; count > 1; count--) { fact = fact * count; } return fact; }}
Il risultato del metodo call() viene restituito all’interno di un oggetto Futuro:
@Testpublic void whenTaskSubmitted_ThenFutureResultObtained(){ FactorialTask task = new FactorialTask(5); Future<Integer> future = executorService.submit(task); assertEquals(120, future.get().intValue());}
Gestione delle eccezioni
Vediamo quanto sono adatti per la gestione delle eccezioni.
4.1. Con Runnable
Poiché la firma del metodo non ha la clausola “throws” specificata, non è possibile propagare ulteriori eccezioni controllate.
4.2. Con Callable
Il metodo Callable() contiene la clausola “throws Exception” in modo da poter facilmente propagare ulteriormente le eccezioni controllate:
public class FactorialTask implements Callable<Integer> { // ... public Integer call() throws InvalidParamaterException { if(number < 0) { throw new InvalidParamaterException("Number should be positive"); } // ... }}
In caso di esecuzione di un Callable utilizzando un ExecutorService, le eccezioni vengono raccolte nell’oggetto Future, che può essere controllato effettuando una chiamata al Future.metodo get (). Questo genererà un’eccezione ExecutionException-che avvolge l’eccezione originale:
@Test(expected = ExecutionException.class)public void whenException_ThenCallableThrowsIt() { FactorialCallableTask task = new FactorialCallableTask(-5); Future<Integer> future = executorService.submit(task); Integer result = future.get().intValue();}
Nel test precedente, ExecutionException viene lanciato mentre stiamo passando un numero non valido. Possiamo chiamare il metodo getCause () su questo oggetto exception per ottenere l’eccezione selezionata originale.
Se non facciamo la chiamata al metodo get() della Futura classe – quindi l’eccezione generata dal metodo call() non essere segnalato, e il compito sarà ancora contrassegnata come completata:
@Testpublic void whenException_ThenCallableDoesntThrowsItIfGetIsNotCalled(){ FactorialCallableTask task = new FactorialCallableTask(-5); Future<Integer> future = executorService.submit(task); assertEquals(false, future.isDone());}
La prova di cui sopra passerà con successo, anche se ci ho buttato un’eccezione per i valori negativi del parametro di FactorialCallableTask.
Conclusione
In questo articolo, abbiamo esplorato le differenze tra le interfacce Runnable e Callable.
Come sempre, il codice completo per questo articolo è disponibile su GitHub.
Inizia con Spring 5 e Spring Boot 2, attraverso il corso Learn Spring:
>> CONTROLLA IL CORSO