
Baeldung Pro comes with both absolutely No-Ads as well as finally with Dark Mode, for a clean learning experience:
Once the early-adopter seats are all used, the price will go up and stay at $33/year.
Last updated: May 11, 2024
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.
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.
Let’s look deeper at how these interfaces handle return values.
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.
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());
}
Let’s see how suitable they are for exception handling.
Since the method signature does not have the “throws” clause specified, we don’t have a way to propagate further checked exceptions.
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.
In this article, we explored the differences between the Runnable and Callable interfaces.