Callable
Definition:
The Callable
interface, introduced in Java 5 as part of the java.util.concurrent
package, is a functional interface that represents a task that can be executed concurrently and returns a result.
Purpose:
While similar to the Runnable
interface, the key distinction of Callable
is its ability to return a result or throw an exception. This makes it suitable for tasks where you need to obtain a computed result.
Using Callable:
Implementing the Callable Interface:
Let's begin by creating a class that implements the Callable
interface.
Example:
import java.util.concurrent.Callable;
public class SquareCalculator implements Callable<Integer> {
private int number;
public SquareCalculator(int number) {
this.number = number;
}
@Override
public Integer call() throws Exception {
System.out.println("Calculating square for: " + number);
return number * number;
}
}
Here, SquareCalculator
is a class implementing Callable<Integer>
. The call
method contains the logic to calculate the square of a given number.
Creating Threads with Callable:
Now, let's use our SquareCalculator
class to create threads that execute the defined task and return results.
Example:
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class Main {
public static void main(String[] args) {
// Creating an instance of the class implementing Callable
SquareCalculator squareCalculator = new SquareCalculator(5);
// Creating a thread pool
ExecutorService executorService = Executors.newSingleThreadExecutor();
// Submitting the Callable to the thread pool
Future<Integer> future = executorService.submit(squareCalculator);
try {
// Getting the result from the Callable
Integer result = future.get();
System.out.println("Result: " + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
} finally {
// Shutting down the executor service
executorService.shutdown();
}
}
}
In this example, a single-threaded executor service is created, and the SquareCalculator
is submitted to it. The result is obtained using the Future
object returned by the submit
method.
Callable with Lambda Expression:
In addition to the traditional approach of creating a class implementing Callable
, Java 8 introduced lambda expressions, providing a more concise way to define Callable
instances. Let's see how we can achieve the same square calculation task using lambda expressions.
Example:
import java.util.concurrent.*;
public class LambdaCallableExample {
public static void main(String[] args) {
// Creating a thread pool
ExecutorService executorService = Executors.newSingleThreadExecutor();
// Submitting a Callable with lambda expression
Future<Integer> future = executorService.submit(() -> {
int number = 5;
System.out.println("Calculating square for: " + number);
return number * number;
});
try {
// Getting the result from the Callable
Integer result = future.get();
System.out.println("Result: " + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
} finally {
// Shutting down the executor service
executorService.shutdown();
}
}
}
In this example, the submit
method is used to submit a Callable
created with a lambda expression to the thread pool. The lambda expression defines the square calculation logic, making the code more compact.
Callable with Multiple Tasks:
Callable
is particularly useful when dealing with multiple tasks concurrently. Let's extend our example to calculate squares for a range of numbers using lambda expressions.
Example:
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
public class MultipleTasksExample {
public static void main(String[] args) {
// Creating a list of tasks using lambda expressions
List<Callable<Integer>> tasks = new ArrayList<>();
for (int i = 1; i <= 5; i++) {
int finalI = i; // Required for using in lambda expression
tasks.add(() -> {
System.out.println("Calculating square for: " + finalI);
return finalI * finalI;
});
}
// Creating a thread pool
ExecutorService executorService = Executors.newFixedThreadPool(3);
try {
// Submitting all tasks and getting a list of Futures
List<Future<Integer>> futures = executorService.invokeAll(tasks);
// Processing the results
for (Future<Integer> future : futures) {
Integer result = future.get();
System.out.println("Result: " + result);
}
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
} finally {
// Shutting down the executor service
executorService.shutdown();
}
}
}
In this example, a list of Callable
tasks is created using lambda expressions. The invokeAll
method is used to submit all tasks and obtain a list of Future
objects representing the results.
Benefits of Callable:
Return Values:
- Unlike
Runnable
,Callable
can return a result, allowing you to obtain the outcome of the computation.
Exception Handling:
- The
call
method ofCallable
can throw checked exceptions, providing better control over error handling.
Multiple Results:
Callable
is suitable for scenarios where you have multiple tasks and need to collect results from each task.
Use the Callable
interface when you need to return a result from a concurrent task. It provides a more flexible alternative to the Runnable
interface.
Compare the error-handling mechanisms of Callable
with those of Runnable
. How does each approach handle exceptions thrown by the concurrent task?