What is the difference between Callable and Runnable in Java?
Table of Contents
Runnable has a void run() method — no return value, no checked exceptions. Callable<V> has a V call() throws Exception method — it returns a result and can throw checked exceptions.
1. Short Answer
Both Runnable and Callable represent tasks that can be executed asynchronously, but they differ in two fundamental ways:
- Return value:
Runnable.run()returnsvoid.Callable.call()returns a typed valueV. - Exception handling:
Runnable.run()cannot throw checked exceptions.Callable.call()declaresthrows Exception, so checked exceptions propagate to the caller viaFuture.get().
Both are functional interfaces and both can be submitted to an ExecutorService, but only Callable gives you a meaningful Future result.
2. Runnable Interface
Runnable is the original Java concurrency interface, present since Java 1.0. Its signature is simple:
@FunctionalInterface
public interface Runnable {
void run(); // No return value, no checked exceptions
}
Key characteristics of Runnable:
- The method returns
void— the task produces no result. - No checked exceptions are declared — any checked exception must be caught inside
run(). - Can be passed directly to a
Threadconstructor or toExecutorService.execute(). - When submitted via
ExecutorService.submit(Runnable), the returnedFuture<?>always returnsnullfromget().
3. Callable Interface
Callable was introduced in Java 5 as part of java.util.concurrent to overcome Runnable's limitations. Its signature is:
@FunctionalInterface
public interface Callable {
V call() throws Exception; // Returns a value, can throw checked exceptions
}
Key characteristics of Callable:
- Generic — the type parameter
Vspecifies the result type. - The method returns a value of type
V. - Declares
throws Exception— checked exceptions propagate naturally. - Must be submitted via
ExecutorService.submit(Callable), which returns aFuture<V>. - Cannot be passed directly to a
Threadconstructor (Thread only accepts Runnable).
Callable Cannot Start a Thread Directly
Unlike Runnable, you cannot do new Thread(callable).start(). Callable tasks must be submitted to an ExecutorService or wrapped with FutureTask.
4. Key Differences
| Feature | Runnable | Callable<V> |
|---|---|---|
| Package | java.lang | java.util.concurrent |
| Since | Java 1.0 | Java 5 |
| Method name | run() | call() |
| Return type | void | V (generic) |
| Checked exceptions | Not allowed | Allowed (throws Exception) |
| Thread constructor | Yes — new Thread(runnable) | No |
| execute() support | Yes — executor.execute(runnable) | No |
| submit() return | Future<?> (get() returns null) | Future<V> (get() returns result) |
5. Code Example
The following example shows both interfaces in action with an ExecutorService:
import java.util.concurrent.*;
public class CallableVsRunnableDemo {
public static void main(String[] args) throws Exception {
ExecutorService executor = Executors.newFixedThreadPool(2);
// --- Runnable: fire-and-forget, no result ---
Runnable runnable = () -> {
System.out.println("Runnable running on: " + Thread.currentThread().getName());
// Must catch checked exceptions here — cannot declare them
};
Future> runnableFuture = executor.submit(runnable);
runnableFuture.get(); // Blocks until done; always returns null
System.out.println("Runnable done. Result: " + runnableFuture.get()); // null
// --- Callable: returns a result, can throw checked exceptions ---
Callable callable = () -> {
System.out.println("Callable running on: " + Thread.currentThread().getName());
int sum = 0;
for (int i = 1; i <= 100; i++) sum += i;
return sum; // Returns a real value
};
Future callableFuture = executor.submit(callable);
Integer result = callableFuture.get(); // Blocks and retrieves the result
System.out.println("Callable result (sum 1-100): " + result); // 5050
// --- Callable with checked exception ---
Callable riskyCallable = () -> {
// Can throw checked exceptions — no try/catch needed here
if (Math.random() > 0.5) {
throw new Exception("Something went wrong in the task");
}
return "Success";
};
Future riskyFuture = executor.submit(riskyCallable);
try {
String value = riskyFuture.get(); // May throw ExecutionException
System.out.println("Risky callable result: " + value);
} catch (ExecutionException e) {
// The checked exception is wrapped in ExecutionException
System.out.println("Task failed: " + e.getCause().getMessage());
}
executor.shutdown();
}
}
// Possible output:
// Runnable running on: pool-1-thread-1
// Runnable done. Result: null
// Callable running on: pool-1-thread-2
// Callable result (sum 1-100): 5050
// Task failed: Something went wrong in the task
6. Using ExecutorService: submit() vs execute()
6.1 execute() — Runnable only
ExecutorService.execute(Runnable) runs the task but returns nothing. Exceptions thrown inside are sent to the thread's UncaughtExceptionHandler and are not propagated to the caller.
executor.execute(() -> System.out.println("Fire and forget"));
// No return value, no way to check completion or catch exceptions
6.2 submit() — Runnable or Callable
ExecutorService.submit() accepts both. It always returns a Future:
// submit(Runnable) → Future>
Future> f1 = executor.submit(() -> System.out.println("Runnable"));
f1.get(); // null — but useful to know when the task finished
// submit(Callable) → Future
Future f2 = executor.submit(() -> "Hello from Callable");
String s = f2.get(); // "Hello from Callable"
// submit(Runnable, T result) → Future
// Lets you specify what get() returns for a Runnable
Future f3 = executor.submit(() -> System.out.println("Runnable"), "done");
String r = f3.get(); // "done"
submit() over execute(). The returned Future lets you detect task completion, retrieve results, and handle exceptions — even for Runnable tasks.
7. When to Use Each
Use Runnable when:
- The task produces no result (e.g., logging, sending a notification, updating a UI element).
- You are passing the task to a
Threadconstructor. - You want the simplest possible representation of a background task.
Use Callable when:
- The task computes and returns a value (e.g., fetching data from a database, calling an API).
- The task may throw a checked exception that the caller must handle.
- You need to use
Futureto retrieve the result, check task status, or cancel the task. - You are submitting multiple tasks with
invokeAll()orinvokeAny(), which requireCallable.
8. Why This Question Matters in Interviews
This is a standard concurrency question that tests whether you understand Java's task abstraction model. Strong answers should include:
- The precise method signatures:
void run()vsV call() throws Exception. - The fact that
Callablewas added in Java 5 withjava.util.concurrent. - How exceptions behave differently: wrapped in
ExecutionExceptionforCallable. - The relationship between
Callable,Future, andFutureTask. - Practical examples: use
Runnablefor background jobs,Callablefor parallel computations that return results.
9. Conclusion
Runnable and Callable are both ways to represent asynchronous tasks in Java. Choose Runnable for simple fire-and-forget tasks that produce no result. Choose Callable when you need the task to return a computed value or propagate checked exceptions. Both can be submitted to an ExecutorService via submit(), but only Callable gives you a Future<V> with a meaningful return value. Understanding when to use each is a sign of solid concurrency knowledge in Java interviews.