Aperçu
Depuis les premiers jours de Java, le multithreading est un aspect majeur du langage. Runnable est l’interface principale fournie pour représenter les tâches multithread et Callable est une version améliorée de Runnable qui a été ajoutée dans Java 1.5.
Dans cet article, nous explorerons les différences et les applications des deux interfaces.
Mécanisme d’exécution
Les deux interfaces sont conçues pour représenter une tâche pouvant être exécutée par plusieurs threads. Les tâches exécutables peuvent être exécutées à l’aide de la classe de thread ou de ExecutorService, tandis que les Callables ne peuvent être exécutées qu’à l’aide de cette dernière.
Valeurs de retour
Examinons de plus près la façon dont ces interfaces gèrent les valeurs de retour.
3.1. Avec Runnable
L’interface Runnable est une interface fonctionnelle et possède une seule méthode run() qui n’accepte aucun paramètre et ne renvoie aucune valeur.
Cela convient aux situations où nous ne recherchons pas de résultat de l’exécution du thread, par exemple, la journalisation des événements entrants:
public interface Runnable { public void run();}
Comprenons cela avec un exemple:
public class EventLoggingTask implements Runnable{ private Logger logger = LoggerFactory.getLogger(EventLoggingTask.class); @Override public void run() { logger.info("Message"); }}
Dans cet exemple, le thread va simplement lire un message de la file d’attente et le consigner dans un fichier journal. Aucune valeur n’est renvoyée par la tâche ; la tâche peut être lancée à l’aide d’ExecutorService:
public void executeTask() { executorService = Executors.newSingleThreadExecutor(); Future future = executorService.submit(new EventLoggingTask()); executorService.shutdown();}
Dans ce cas, le Futur objet ne détiendra aucune valeur.
3.2. Avec Callable
L’interface Callable est une interface générique contenant une seule méthode call() – qui renvoie une valeur générique V:
public interface Callable<V> { V call() throws Exception;}
Jetons un coup d’œil au calcul de la factorielle d’un nombre:
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; }}
Le résultat de la méthode call() est renvoyé dans un objet futur:
@Testpublic void whenTaskSubmitted_ThenFutureResultObtained(){ FactorialTask task = new FactorialTask(5); Future<Integer> future = executorService.submit(task); assertEquals(120, future.get().intValue());}
Gestion des exceptions
Voyons à quel point elles conviennent à la gestion des exceptions.
4.1. Avec Runnable
Puisque la signature de la méthode n’a pas la clause « throws » spécifiée, il n’y a aucun moyen de propager d’autres exceptions vérifiées.
4.2. Avec Callable
La méthode call() de Callable contient la clause « throws Exception » afin que nous puissions facilement propager davantage les exceptions vérifiées:
public class FactorialTask implements Callable<Integer> { // ... public Integer call() throws InvalidParamaterException { if(number < 0) { throw new InvalidParamaterException("Number should be positive"); } // ... }}
En cas d’exécution d’un appelable à l’aide d’un ExecutorService, les exceptions sont collectées dans l’objet Futur, qui peut être vérifié en faisant un appel au Futur.méthode get(). Cela lancera une ExecutionException – qui enveloppe l’exception d’origine:
@Test(expected = ExecutionException.class)public void whenException_ThenCallableThrowsIt() { FactorialCallableTask task = new FactorialCallableTask(-5); Future<Integer> future = executorService.submit(task); Integer result = future.get().intValue();}
Dans le test ci-dessus, l’exception ExecutionException est levée car nous passons un nombre invalide. Nous pouvons appeler la méthode getCause() sur cet objet exception pour obtenir l’exception vérifiée d’origine.
Si nous ne faisons pas l’appel à la méthode get() de la classe Future, l’exception levée par la méthode call() ne sera pas rapportée et la tâche sera toujours marquée comme terminée:
@Testpublic void whenException_ThenCallableDoesntThrowsItIfGetIsNotCalled(){ FactorialCallableTask task = new FactorialCallableTask(-5); Future<Integer> future = executorService.submit(task); assertEquals(false, future.isDone());}
Le test ci-dessus passera avec succès même si nous avons lancé une exception pour les valeurs négatives du paramètre à FactorialCallableTask.
Conclusion
Dans cet article, nous avons exploré les différences entre les interfaces Exécutables et Appelables.
Comme toujours, le code complet de cet article est disponible sur GitHub.
Commencez avec Spring 5 et Spring Boot 2, grâce au cours Learn Spring:
>> DÉCOUVREZ LE COURS