Skip to main content

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 of Callable 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.
tip

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.

Challenge Question

Compare the error-handling mechanisms of Callable with those of Runnable. How does each approach handle exceptions thrown by the concurrent task?