← Back to Multithreading Course

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.

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.

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:

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.

Next Class: Getting Results with Future

We have learned that Callable can return a result. But when we submit a Callable task to an ExecutorService, how do we actually receive that result?

In the next class, we will understand the Future interface and its get() method.

We will learn how to retrieve results from tasks that execute in separate worker threads.

Let's keep learning, one concept at a time!

0% Completed

You have completed 30 out of 46 classes.