1. Overview
Since Java’s early days, multithreading has been a major aspect of the language. Runnable is the core interface provided for representing multithreaded tasks, and Java 1.5 provided Callable as an improved version of Runnable.
In this tutorial, we’ll explore the differences and the applications of both interfaces.
2. Execution Mechanism
Both interfaces are designed to represent a task that can be run by multiple threads. We can run Runnable tasks using the Thread class or ExecutorService, whereas we can only run Callables using the latter.
3. Return Values
Let’s look deeper at how these interfaces handle return values.
3.1. With Runnable
The Runnable interface is a functional interface and has a single run() method that doesn’t accept any parameters or return any values.
This works for situations where we aren’t looking for a result of the thread execution, such as incoming events logging:
public interface Runnable {
public void run();
}
Let’s understand this with an example:
public class EventLoggingTask implements Runnable{
private Logger logger
= LoggerFactory.getLogger(EventLoggingTask.class);
@Override
public void run() {
logger.info("Message");
}
}
In this example, the thread will just read a message from the queue and log it in a log file. There’s no value returned from the task.
We can launch the task using ExecutorService:
public void executeTask() {
executorService = Executors.newSingleThreadExecutor();
Future future = executorService.submit(new EventLoggingTask());
executorService.shutdown();
}
In this case, the Future object will not hold any value.
3.2. With Callable
The Callable interface is a generic interface containing a single call() method that returns a generic value V:
public interface Callable<V> {
V call() throws Exception;
}
Let’s look at calculating the factorial of a number:
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;
}
}
The result of call() method is returned within a Future object:
@Test
public void whenTaskSubmitted_ThenFutureResultObtained(){
FactorialTask task = new FactorialTask(5);
Future<Integer> future = executorService.submit(task);
assertEquals(120, future.get().intValue());
}
4. Exception Handling
Let’s see how suitable they are for exception handling.
4.1. With Runnable
Since the method signature does not have the “throws” clause specified, we don’t have a way to propagate further checked exceptions.
4.2. With Callable
Callable‘s call() method contains the “throws Exception” clause, so we can easily propagate checked exceptions further:
public class FactorialTask implements Callable<Integer> {
// ...
public Integer call() throws InvalidParamaterException {
if(number < 0) {
throw new InvalidParamaterException("Number should be positive");
}
// ...
}
}
In case of running a Callable using an ExecutorService, the exceptions are collected in the Future object. We can check this by making a call to the Future.get() method.
This will throw an ExecutionException, which wraps the original exception:
@Test(expected = ExecutionException.class)
public void whenException_ThenCallableThrowsIt() {
FactorialCallableTask task = new FactorialCallableTask(-5);
Future<Integer> future = executorService.submit(task);
Integer result = future.get().intValue();
}
In the above test, the ExecutionException is thrown since we are passing an invalid number. We can call the getCause() method on this exception object to get the original checked exception.
If we don’t make the call to the get() method of Future class, the exception thrown by call() method will not be reported back, and the task will still be marked as completed:
@Test
public void whenException_ThenCallableDoesntThrowsItIfGetIsNotCalled(){
FactorialCallableTask task = new FactorialCallableTask(-5);
Future<Integer> future = executorService.submit(task);
assertEquals(false, future.isDone());
}
The above test will pass successfully even though we’ve thrown an exception for the negative values of the parameter to FactorialCallableTask.
5. Conclusion
In this article, we explored the differences between the Runnable and Callable interfaces.
As always, the complete code for this article is available over on GitHub.