Java provides robust multithreading capabilities through the java.lang and java.util.concurrent packages. Two essential interfaces for creating tasks that can run concurrently are Runnable and Callable. While both are used to encapsulate tasks meant to run in a separate thread, they have key differences in how they are implemented and what they return.
1. Runnable Interface: Overview
The Runnable interface has been part of Java since version 1.0. It represents a task that can be executed by a thread. It does not return any result and cannot throw a checked exception.
public interface Runnable {
void run();
}
Example of Runnable
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Task executed by: " + Thread.currentThread().getName());
}
}
public class RunnableExample {
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.start();
}
}
- Does not return any result.
- Cannot throw checked exceptions.
- Used with
ThreadorExecutorService.
2. Callable Interface: Overview
The Callable interface was introduced in Java 5 as part of the java.util.concurrent package. It is designed to return a result and may throw a checked exception.
public interface Callable<V> {
V call() throws Exception;
}
Example of Callable
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
return "Task executed by: " + Thread.currentThread().getName();
}
}
public class CallableExample {
public static void main(String[] args) throws Exception {
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(new MyCallable());
System.out.println(future.get()); // Waits and retrieves the result
executor.shutdown();
}
}
- Returns a result of a generic type.
- Can throw checked exceptions.
- Must be used with
ExecutorServiceandFuture.
3. Runnable vs Callable: Detailed Comparison
| Feature | Runnable | Callable |
|---|---|---|
| Return Type | void | Generic type (e.g., String, Integer) |
| Throws Exceptions | Cannot throw checked exceptions | Can throw checked exceptions |
| Used With | Thread or ExecutorService |
Only with ExecutorService |
| Available Since | Java 1.0 | Java 5 |
4. When Should You Use Runnable?
Use Runnable when:
- You don’t need to return a result from the task.
- Your code is simple and doesn’t throw checked exceptions.
- You are using traditional threads or basic task execution.
Example: Using Runnable with ExecutorService
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.execute(() -> System.out.println("Runnable task running"));
executor.shutdown();
5. When Should You Use Callable?
Use Callable when:
- You need the task to return a result.
- You need to handle checked exceptions.
- You are using concurrent task execution with
ExecutorService.
Example: Callable with Multiple Threads
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.*;
public class MultipleCallableExample {
public static void main(String[] args) throws InterruptedException, ExecutionException {
ExecutorService executor = Executors.newFixedThreadPool(3);
List<Callable<String>> tasks = Arrays.asList(
() -> "Task 1 result",
() -> "Task 2 result",
() -> "Task 3 result"
);
List<Future<String>> futures = executor.invokeAll(tasks);
for (Future<String> future : futures) {
System.out.println(future.get());
}
executor.shutdown();
}
}
6. Handling Exceptions in Callable
The call() method in Callable allows us to throw exceptions, making it a better choice for tasks where error handling is important.
class ErrorProneCallable implements Callable<Integer> {
public Integer call() throws Exception {
throw new Exception("Simulated error");
}
}
7. Using Lambda with Runnable and Callable
// Runnable with Lambda
Runnable r = () -> System.out.println("Runnable Lambda");
// Callable with Lambda
Callable<String> c = () -> "Callable Lambda";
8. Real-World Use Case: Runnable vs Callable
Example: Logging Service (Runnable)
Use Runnable when you’re just saving logs in a background thread, no return needed:
executor.execute(() -> logToFile("User logged in at " + LocalDateTime.now()));
Example: Data Fetching Service (Callable)
Use Callable when fetching something from a database:
Callable<User> fetchUser = () -> db.getUserById("123");
Future<User> userFuture = executor.submit(fetchUser);
User user = userFuture.get();