Class 30: Runnable vs. Callable
Part 5 — Managing Threads with the Executor Framework
In our previous classes, we used the Runnable
interface to define tasks that could be executed by threads
and the Executor Framework.
But let's think about a situation.
Imagine we submit a task that calculates the total price of a shopping cart.
The task performs the calculation, but we also want to get the calculated amount back.
Can Runnable return a result?
No. A Runnable task does not return a result.
Java provides another interface for tasks that need to return a result. It is called Callable.
In this class, we will understand both interfaces and when to use each one.
1. Understanding Runnable
We have already used Runnable many times in this course.
Runnable is an interface used to represent a task that needs to be executed.
It provides one main method:
void run();
Notice that the method has a void return type.
This means a Runnable task performs some work but does not return a result through its run() method.
Example: Runnable
public class RunnableExample {
public static void main(String[] args) {
Runnable task = () -> {
System.out.println("Task is running");
};
Thread thread = new Thread(task);
thread.start();
}
}
Let's understand this example.
- We create a Runnable task.
- The task prints a message.
- We pass the task to a Thread.
- We call start() to execute it on a separate thread.
The task performs its work, but it does not return a value.
2. What If We Need a Return Value?
Let's say we want to calculate the sum of two numbers.
We want our task to calculate the sum and give the result back to us.
A Runnable task cannot return a value directly because its run() method has a void return type.
We could use shared variables or other techniques to communicate the result, but Java provides a more suitable interface for this.
That interface is Callable.
3. Understanding Callable
Callable is an interface used to represent a task that can return a result.
It provides a method called call().
V call() throws Exception;
Let's understand this method.
Vrepresents the type of value returned by the task.call()contains the actual work to be performed.throws Exceptionmeans the method can throw an exception.
For example, if our task returns an integer, we can use
Callable<Integer>.
If it returns a String, we can use Callable<String>.
The generic type tells Java what kind of result the task will return.
4. Our First Callable Example
Let's create a Callable task that calculates the sum of two numbers.
import java.util.concurrent.Callable;
public class CallableExample {
public static void main(String[] args) throws Exception {
Callable<Integer> task = () -> {
int a = 10;
int b = 20;
return a + b;
};
Integer result = task.call();
System.out.println("Result: " + result);
}
}
Let's understand this example.
Step 1: Create a Callable Task
Callable<Integer> task = () -> {
int a = 10;
int b = 20;
return a + b;
};
We create a Callable task that returns an Integer.
The task calculates the sum of two numbers and returns the result.
Step 2: Call the Task
Integer result = task.call();
Here, we directly call the call() method. The method executes the task and returns its result.
Notice that we are calling it directly in this example. Therefore, it executes on the current thread.
We will see how to execute Callable tasks using ExecutorService in the next section.
5. Executing Callable Using ExecutorService
In real applications, we usually want the Callable task to be executed by a worker thread managed by an ExecutorService.
ExecutorService provides a method called submit()
that can accept a Callable task.
Let's write a complete example.
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class CallableExecutorExample {
public static void main(String[] args) {
ExecutorService executorService =
Executors.newSingleThreadExecutor();
Callable<Integer> task = () -> {
System.out.println(
"Calculating on: " +
Thread.currentThread().getName()
);
int a = 10;
int b = 20;
return a + b;
};
Future<Integer> future =
executorService.submit(task);
executorService.shutdown();
}
}
We have submitted the Callable task to the ExecutorService. But how do we get the returned result?
For that, we need to understand the Future interface.
We will study Future in detail in the next class.
For now, remember that submitting a Callable task using
submit() returns a Future object representing
the task's result.
6. Runnable vs. Callable
Let's compare the two interfaces.
| Runnable | Callable |
|---|---|
| Used to represent a task. | Used to represent a task. |
| Provides the run() method. | Provides the call() method. |
| run() returns void. | call() returns a value of type V. |
| Cannot declare checked exceptions in its method signature. | Can declare checked exceptions using throws Exception. |
| Can be passed to Thread or submitted to ExecutorService. | Can be submitted to ExecutorService. |
| Useful when we do not need a result from the task. | Useful when we need a result from the task. |
7. When Should We Use Runnable and Callable?
Let's understand this using simple real-world examples.
Use Runnable When We Don't Need a Return Value
Imagine we want to send a notification email.
We simply want the task to perform the operation. We do not need the task to return a calculation result.
Runnable emailTask = () -> {
System.out.println("Sending notification email");
};
Use Callable When We Need a Return Value
Imagine we want to calculate the total amount of an order.
We want the task to perform the calculation and return the amount.
Callable<Double> calculationTask = () -> {
double price = 1000;
double tax = 180;
return price + tax;
};
The choice is simple:
- Need to perform work without returning a result? Use Runnable.
- Need to perform work and return a result? Use Callable.
8. Can Callable Be Used Directly with Thread?
No, not directly.
The Thread class provides constructors that accept Runnable. Callable does not implement Runnable.
Therefore, we cannot directly pass a Callable object to a Thread constructor.
Callable is designed to work with task execution mechanisms such as ExecutorService.
For now, remember that we normally submit Callable tasks
using the submit() method of ExecutorService.
9. What We Learned Today
We learned about two interfaces that represent tasks in Java.
Runnable is suitable when we simply want to perform some work. Callable is suitable when we want to perform work and return a result.
In the next class, we will understand how Future helps us retrieve the result of a Callable task.
Test Your Understanding
Let's see how well you understood this class!
Question 1: Which method does the Runnable interface provide?
Question 2: Which interface should we use when a task needs to return a result?
Question 3: What does Callable<Integer> mean?
Question 4: Which method is used to submit a Callable task to an ExecutorService?
Question 5: Can we directly pass a Callable object to a Thread constructor?
Class Summary
Let's quickly revise what we learned today.
- Runnable represents a task that does not return a result.
- Runnable provides the
run()method. - Callable represents a task that can return a result.
- Callable provides the
call()method. - Callable uses a generic type to specify its return value.
- Callable can declare checked exceptions.
- ExecutorService can execute Callable tasks using
submit().
Remember: Runnable is for tasks that do not return a result. Callable is for tasks that return a result.
You have completed 30 out of 46 classes.