Definition
- Runnable – a functional interface with
void run()that executes a task returning nothing and cannot declare checked exceptions. - Callable
– a functional interface with V call() throws Exceptionthat executes a task, returns a result of type V, and can throw any checked exception. - Future
– the handle returned by submit(Callable)that lets you retrieve the Callable's result withget(), which blocks until the value is ready. - Executors.callable() – a utility method that wraps a Runnable into a Callable, with an optional preset return value.
- Exception propagation – Callable exceptions are captured in the Future and surface as
ExecutionExceptionwhenget()is called.
At National Bank of India, Runnable is a task form that says "Send an SMS to Rajesh about his trade settlement." No receipt, no response expected. Priya hands it to the teller and walks away. Callable is a form that says "Calculate Rajesh's portfolio value and give me the number." Priya gets a receipt (Future) and comes back later to collect the result: Rs.15,00,000. If the calculation failed, the receipt tells her why.
Understanding the Concepts
What is the difference between Callable and Runnable?
Both Runnable and Callable represent tasks that can be executed by threads. The fundamental difference is that Callable returns a result and can throw checked exceptions, while Runnable does neither.
| Feature | Runnable | Callable
|---|---|---|
| Method signature | void run() | V call() throws Exception |
| Return value | None | V (any type) |
| Checked exceptions | Cannot throw | Can throw any Exception |
| Functional interface | Yes (since Java 8) | Yes (since Java 8) |
| Thread constructor | Accepted | Not accepted |
| submit() return | Future> (get() returns null) | Future (get() returns V) |
package com.bank.service;
import java.util.concurrent.*;
import java.math.BigDecimal;
public class CallableVsRunnable {
public static void main(String[] args) throws Exception {
ExecutorService pool = Executors.newFixedThreadPool(2);
// Runnable: no return, no checked exception
Runnable notification = () -> {
System.out.println("SMS sent to Rajesh: Trade TRD-001 settled");
};
pool.execute(notification);
// Callable: returns BigDecimal, throws checked exception
Callable<BigDecimal> navCalc = () -> {
BigDecimal nav = new BigDecimal("1500000.00");
if (nav.compareTo(BigDecimal.ZERO) <= 0) {
throw new Exception("NAV calculation failed for FUND-001");
}
return nav;
};
Future<BigDecimal> future = pool.submit(navCalc);
System.out.println("NAV: Rs." + future.get());
pool.shutdown();
}
}
SMS sent to Rajesh: Trade TRD-001 settled
NAV: Rs.1500000.00
Why can Runnable not throw checked exceptions?
Runnable's run() method has no throws clause in its signature. This means any lambda or implementation of Runnable cannot declare checked exceptions. Only unchecked exceptions (RuntimeException and its subclasses) can escape from run(). If you need to handle IO failures or SQL errors inside a Runnable, you must catch them inside the method body.
// WRONG: compile error, run() has no throws clause
Runnable bad = () -> {
// throw new IOException("File not found"); // COMPILE ERROR
};
// Workaround: catch inside run()
Runnable workaround = () -> {
try {
java.nio.file.Files.readString(java.nio.file.Path.of("accounts.csv"));
} catch (java.io.IOException e) {
throw new RuntimeException("Failed to read accounts", e);
}
};
// Callable: checked exceptions are fine
Callable<String> clean = () -> {
return java.nio.file.Files.readString(java.nio.file.Path.of("accounts.csv"));
// IOException propagates naturally through call()
};
How do you retrieve the result from a Callable?
Submit the Callable to an ExecutorService using submit(). This returns a Future that holds the pending result. Call future.get() to block until the result is ready, or future.get(timeout, unit) to block with a deadline.
package com.bank.service;
import java.util.concurrent.*;
import java.math.BigDecimal;
public class RetrieveResult {
public static void main(String[] args) throws Exception {
ExecutorService pool = Executors.newFixedThreadPool(2);
Future<BigDecimal> balanceFuture = pool.submit(() -> {
Thread.sleep(200); // simulate DB call
return new BigDecimal("250000.50");
});
// Do other work while the balance is being fetched
System.out.println("Preparing statement...");
// Now block to get the result
BigDecimal balance = balanceFuture.get(5, TimeUnit.SECONDS);
System.out.println("Account SAV2001 balance: Rs." + balance);
pool.shutdown();
}
}
Preparing statement...
Account SAV2001 balance: Rs.250000.50
How does exception handling differ between Runnable and Callable?
With execute(Runnable), if the task throws an unchecked exception, it goes to the thread's UncaughtExceptionHandler. The worker thread dies and the pool creates a replacement. The exception may be silently lost if no handler is configured.
With submit(Callable), any exception is captured inside the Future. The worker thread survives. The exception surfaces only when future.get() is called, wrapped in an ExecutionException.
package com.bank.service;
import java.util.concurrent.*;
public class ExceptionHandling {
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(2);
// submit(): exception captured in Future
Future<?> future = pool.submit(() -> {
throw new RuntimeException("Settlement failed for TRD-002");
});
try {
future.get();
} catch (ExecutionException e) {
// Unwrap to get the original exception
System.out.println("Cause: " + e.getCause().getMessage());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
pool.shutdown();
}
}
Cause: Settlement failed for TRD-002
How do you convert a Runnable to a Callable?
The Executors utility class provides callable(Runnable) and callable(Runnable, T result) to wrap a Runnable as a Callable.
Runnable task = () -> System.out.println("Audit log written");
Callable<Object> c1 = Executors.callable(task); // get() returns null
Callable<String> c2 = Executors.callable(task, "DONE"); // get() returns "DONE"
When should you use Runnable vs Callable?
Use Runnable for fire-and-forget work where you do not need the result: sending notifications, writing audit logs, warming caches, and cleanup tasks.
Use Callable when you need the computation result: balance lookups, interest calculations, credit score fetches, API calls, and any task where the caller needs to know the outcome.
- EOD batch notifications (Runnable) – send 10,000 SMS messages, no result needed.
- EOD interest calculation (Callable) – compute interest for 50,000 accounts, collect each amount.
- Statement generation (Callable) – generate PDF, return file path.
- Cache refresh (Runnable) – reload price data in background.
What about thread safety when sharing data between tasks?
Both Runnable and Callable tasks run on pool threads. If multiple tasks share mutable state, synchronization is required. Use BigDecimal for money calculations, ConcurrentHashMap for shared maps, and AtomicLong for counters.
What are common interview follow-up questions?
- Can you use Callable with
new Thread()? No, Thread's constructor only accepts Runnable. - What does
Future.get()return when you submit a Runnable? It returns null. - How do you handle timeouts when waiting for a Callable result? Use
get(timeout, unit)which throwsTimeoutException. - Can a Callable return void? Use
Callableand return null fromcall().
Visual Overview
Runnable Callable<V>
┌──────────────────┐ ┌──────────────────────────┐
│ void run() │ │ V call() throws Exception │
│ No return │ │ Returns V │
│ No checked exc │ │ Checked exceptions OK │
└────────┬─────────┘ └────────────┬─────────────┘
│ │
▼ ▼
pool.execute(runnable) pool.submit(callable)
│ │
▼ ▼
void (fire-and-forget) Future<V>
│
▼
future.get() → V
(blocks until ready)
Key Takeaway
Runnable and Callable are the two task abstractions in Java concurrency. Runnable has void run() and is for fire-and-forget work: send a notification, write an audit log, warm a cache. Callable has V call() throws Exception and is for compute-and-return work: calculate a balance, fetch a credit score, generate a report.
When you submit a Callable to an ExecutorService, you get a Future that holds the pending result. Calling future.get() blocks until the value is ready. If the task threw an exception, get() wraps it in an ExecutionException so the caller can handle it. Runnable exceptions via execute() go to UncaughtExceptionHandler and may be lost.
In our banking systems, the rule is straightforward. Need the answer? Callable. Do not need the answer? Runnable. Interest calculation for 50,000 accounts: submit Callable tasks, collect BigDecimal results via invokeAll. SMS notifications: submit Runnable tasks via execute, move on.
Remember one sentence: Runnable runs and forgets, Callable calls and returns.
---