Runnable and Callable in Java4 min read

In the early days of Java programming, whenever you want to create a thread, usually you would need the help of whether extending the Thread class or implementing the Runnable interface. For the most part, we usually choose the latter for creating a new thread because implementing an interface doesn’t hinder a class extends another class.

Since Java 1.5, there is another interface called Callable and its job also is to represent a task that can be potentially executed on multiple threads.

Let’s briefly examine both interfaces’ signature and their methods.

For the Runnable interface:

public interface Runnable {
     public void run();
}

For the Callable interface:

public interface Callable<V> {
     public V call() throws Exception;
}

Both the Runnable and Callable each contains only one method and each method accepts no argument. However, there are still some differences between the Callable and Runnable interface, let’s find out.

run() method doesn’t return anything, but call() method does

To create a new thread with the Runnable interface, we must override the run() method:

class RunnableImpl implements Runnable {
     @Override 
     public void run() {
         System.out.println("Some texts from the " + Thread.currentThread().getName());
     }
}

As we can easily notice, with its signature public void run(), the method takes no argument and returns nothing.

On the other hand, the interface Callable<V> accepts a parameterized type of type V, this type V will be used as the return type of its call() method:

class CallableImpl implements Callable<Double> {
     @Override
     public Double call() throws Exception {
         return Math.random();
     }
}

To create a thread with the Callable interface, we must implement its call() method. Unlike the run() method of the Runnable interface which doesn’t return any value, the call() method of the Callable interface returns a result.

The Thread class only accepts an object with the type of Runnable, the Callable object cannot be passed to the Thread class’s constructor, but it can be used in the ExecutorService interface, which we will discuss below.

call() throws a checked exception, but run() doesn’t

Another important point we need to notice here is that the Runnable interface with its run() method doesn’t force you to throw a checked exception, but the call() method of the Callable interface does compel you to throw a checked exception. Of course, the exception type isn’t always be exactly Exception, it can be any other checked exception that you want to throw such as IllegalArgumentExcetion, ClassNotFoundException, etc…

Runnable and Callable with the Future Object

Creating some handful threads by either implementing Runnable or Callable interfaces is fine, but when there are a hundred threads need controlling, this process becomes cumbersome with these low-level approaches.

To simplify this process, the ExecutorService interface lets you encapsulate one or more threads into a single pool and puts submitted tasks in an internal queue to execute them by using the threads. This article is not about ExecutorService, but this interface has one method named submit() which accepts either a Runnable or Callable object:

  • Future<?> submit (Runnable task)
  • <T>Future<T> submit (Callable<T> task)

Either you pass a Runnable object or Callable object to the submit() method of the ExecutorService, it will always return a Future object.

However, if we pass the Runnable object to the submit() method, the returned Future object will not hold any value:

ExecutorService executorService = Executors.newFixedThreadPool(5);
Future<?> futureIsHere = executorService.submit(() -> {
    System.out.println("Hello from: " + Thread.currentThread().getName());
});
executorService.shutDown();
System.out.println(futureIsHere.get()); // null, it holds nothing

If you feel a bit confused about the generic’s wildcard, consider reading this post.

Both the Callable and Runnable are functional interfaces, then we can use lambda expression for creating a new task. The parameterized type of the Future object will have the same type as the return type of the run() method of the Runnable interface. The run() method returns nothing, hence the Future object gets nothing to store because of this fact.

On the other hand, the call() method of the Callable interface does return a value with type T, thus the Future object will have the value of type T:

ExecutorService executorService = Executors.newFixedThreadPool(5);
Future<String> futureIsHere = executorService.submit(new Callable<String>() {
   @Override
   public String call() throws Exception {
      return "Hello from: " + Thread.currentThread().getName();
   }
});
System.out.println(futureIsHere.get()); // Hello from: pool-1-thread-1

In this case, the call() method of the Callable object returns a String, then the Future object will keep a String value. If the parameterized type of Future object and the call() method’s return type is inarticulate, then the code of course will not compile.

Summing up

Finally, let’s quickly recap the distinctions between the Runnable and Callable interfaces:

  • The run() method of the Runnable method doesn’t return any value, yet the call() method of Callable does return a value.
  • The Runnable interface doesn’t compel you to throw any checked exception, but the Callable does.
  • ExecutorService interface has a method named submit(), this method returns a Future object, if a task is submitted as a Runnable object, then the Future object has nothing to preserve. If instead, a Callable object is used for submitting a task, the Future object will have the value with the same type as the return type of the call() method.
Previous Article
Next Article
Every support is much appreciated ❤️

Buy Me a Coffee