1. Información general
Desde los primeros días de Java, el subproceso múltiple ha sido un aspecto importante del lenguaje. Runnable es la interfaz principal proporcionada para representar tareas de subprocesos múltiples y Callable es una versión mejorada de Runnable que se agregó en Java 1.5.
En este artículo, exploraremos las diferencias y las aplicaciones de ambas interfaces.
2. Mecanismo de ejecución
Ambas interfaces están diseñadas para representar una tarea que puede ser ejecutada por varios subprocesos. Las tareas ejecutables se pueden ejecutar usando la clase Thread o ExecutorService, mientras que las Callables solo se pueden ejecutar usando la última.
3. Valores devueltos
Echemos un vistazo más profundo a la forma en que estas interfaces manejan los valores de retorno.
3.1. Con Runnable
La interfaz Runnable es una interfaz funcional y tiene un único método run () que no acepta ningún parámetro y no devuelve ningún valor.
Esto es adecuado para situaciones en las que no buscamos un resultado de la ejecución del hilo, por ejemplo, el registro de eventos entrantes:
public interface Runnable { public void run(); }
Entendamos esto con un ejemplo:
public class EventLoggingTask implements Runnable{ private Logger logger = LoggerFactory.getLogger(EventLoggingTask.class); @Override public void run() { logger.info("Message"); } }
En este ejemplo, el hilo simplemente leerá un mensaje de la cola y lo registrará en un archivo de registro. No se devuelve ningún valor de la tarea; la tarea se puede iniciar usando ExecutorService:
public void executeTask() { executorService = Executors.newSingleThreadExecutor(); Future future = executorService.submit(new EventLoggingTask()); executorService.shutdown(); }
En este caso, el objeto Future no tendrá ningún valor.
3.2. Con invocable
La interfaz invocable es una interfaz genérica que contiene un único método call () , que devuelve un valor genérico V :
public interface Callable { V call() throws Exception; }
Echemos un vistazo al cálculo del factorial de un número:
public class FactorialTask implements Callable { int number; // standard constructors public Integer call() throws InvalidParamaterException { int fact = 1; // ... for(int count = number; count > 1; count--) { fact = fact * count; } return fact; } }
El resultado del método call () se devuelve dentro de un objeto Future :
@Test public void whenTaskSubmitted_ThenFutureResultObtained(){ FactorialTask task = new FactorialTask(5); Future future = executorService.submit(task); assertEquals(120, future.get().intValue()); }
4. Manejo de excepciones
Veamos qué tan adecuados son para el manejo de excepciones.
4.1. Con Runnable
Dado que la firma del método no tiene la cláusula "throws" especificada,no hay forma de propagar más excepciones comprobadas.
4.2. Con invocable
El método call () de Callable contiene la cláusula "throws Exception" para que podamos propagar más fácilmente las excepciones marcadas:
public class FactorialTask implements Callable { // ... public Integer call() throws InvalidParamaterException { if(number < 0) { throw new InvalidParamaterException("Number should be positive"); } // ... } }
En caso de ejecutar un Callable usando un ExecutorService, las excepciones se recopilan en el objeto Future , que se puede verificar haciendo una llamada al método Future.get () . Esto arrojará una ExecutionException, que envuelve la excepción original:
@Test(expected = ExecutionException.class) public void whenException_ThenCallableThrowsIt() { FactorialCallableTask task = new FactorialCallableTask(-5); Future future = executorService.submit(task); Integer result = future.get().intValue(); }
En la prueba anterior, se lanza ExecutionException ya que estamos pasando un número no válido. Podemos llamar al método getCause () en este objeto de excepción para obtener la excepción marcada original.
Si no hacemos la llamada al método get () de la clase Future , entonces la excepción lanzada por el método call () no será reportada y la tarea aún se marcará como completada:
@Test public void whenException_ThenCallableDoesntThrowsItIfGetIsNotCalled(){ FactorialCallableTask task = new FactorialCallableTask(-5); Future future = executorService.submit(task); assertEquals(false, future.isDone()); }
La prueba anterior pasará con éxito aunque hayamos lanzado una excepción para los valores negativos del parámetro a FactorialCallableTask.
5. Conclusión
En este artículo, exploramos las diferencias entre las interfaces Runnable y Callable .
Como siempre, el código completo de este artículo está disponible en GitHub.