Future Interface in Java

Reetesh Kumar
7 min readJan 31, 2024

--

Introduction

The Future interface, part of the java.util.concurrent package represents the result of an asynchronous computation. A Future object returns from a computation when it is complete, but the computation itself runs in a separate thread, allowing the program to continue doing other work in the meantime. In this blog, we’ll delve into the intricacies of the Future interface and illustrate its use with practical examples.

Understanding The Future Interface

A Future is like a promise of a result from a task that’s running in the background. Think of it as a placeholder for the end product of an ongoing process. We have tools at our disposal to check in on this task: Is it done yet? How long until it’s complete? And when it’s finally ready, we can collect the fruits of this labour through the ‘get’ method. But remember, if it’s not finished, ‘get’ will patiently wait until the task concludes.

What if you change your mind? The ‘cancel’ method is there for that. We can also find out whether the task wrapped up on its terms or if it was stopped in its tracks by cancellation. However, cancelling is no longer an option once the task crosses the finish line.

There’s a neat trick for when you want the option to cancel but don’t need the task’s result. We can set up a Future with a wildcard type, like ‘Future<?>’, and have it return ‘null’. This way, we get the flexibility of cancellation without being tied to a specific outcome.

Key Characteristics

  1. Asynchronous Execution: Future allows the execution of long-running operations in a background thread, preventing the blocking of the main thread.
  2. Result Retrieval: It provides methods to check if the computation is complete, to wait for its completion, and to retrieve the result.
  3. Cancelable Operations: A Future can be used to cancel the execution of an asynchronous operation.

Example Use Case

To demonstrate the Future interface, let’s consider an example where we need to perform a complex calculation in a separate thread.

Step 1: Creating an ExecutorService: First, we need an ExecutorService to submit tasks:

ExecutorService executor = Executors.newSingleThreadExecutor();

Step 2: Submitting a Callable Task: We then define and submit a Callable task to the executor:

Future<Integer> futureResult = executor.submit(() -> {
// Simulating a long-running task
TimeUnit.SECONDS.sleep(2);
return 42; // The result of the computation
});

Here, submit() returns a Future representing the pending result.

Step 3: Retrieving the Result: Finally, we retrieve the result of the computation:

try {
Integer result = futureResult.get(); // Blocking call
System.out.println("Result of the computation: " + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}

The get() method blocks until the computation is complete, after which it returns the result.

Output:

Result of the computation: 42

Additional Operations

  1. Check if the Task is Complete: We can check if the computation is done by using futureResult.isDone().
  2. Cancel the Task: If we need to cancel the computation, we can use futureResult.cancel(true).
  3. Timeouts: We can set a timeout on the get() method to prevent indefinite blocking.

Another Example Of Future

Let’s consider a real-world scenario where the Future interface in Java can be effectively utilized. Imagine we have an application for a travel booking system, which needs to fetch flight availability from various airlines. This task involves making network calls to different airline APIs, which can be time-consuming. Using the Future interface, we can handle these calls asynchronously to improve the performance and responsiveness of our application.

Scenario: Travel Booking System — Fetching Flight Data

Background: Our application allows users to search for flights between destinations. When a user initiates a search, the system must query multiple airline APIs to gather available flight options. Each API call is independent and can take several seconds.

We need to first set up a few components:

  1. The List of Airlines: A list of airline names (or codes) that we will iterate over.
  2. The ExecutorService: To manage our asynchronous tasks.
  3. The fetchFlightsFromAirline Method: As previously discussed, this method returns a Callable<List<Flight>> that simulates fetching flight data from an airline.

The Flight Class: First, let’s define a simple Flight class to represent the flight data

public class Flight {
private String airline;
private String from;
private String to;
private LocalDateTime departureTime;
private LocalDateTime arrivalTime;

// Constructor, getters, and setters
public Flight(String airline, String from, String to, LocalDateTime departureTime, LocalDateTime arrivalTime) {
this.airline = airline;
this.from = from;
this.to = to;
this.departureTime = departureTime;
this.arrivalTime = arrivalTime;
}

// ... Additional methods ...
}

The fetchFlightsFromAirline Method: Now, let’s implement the fetchFlightsFromAirline method

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;

public class FlightService {

// Mock method to simulate fetching flights from an airline
public Callable<List<Flight>> fetchFlightsFromAirline(String airline, String from, String to, LocalDateTime date) {
return () -> {
// Simulate network delay
Thread.sleep(2000);

// Mock response data
List<Flight> flights = new ArrayList<>();
flights.add(new Flight(airline, from, to, date, date.plusHours(2)));
flights.add(new Flight(airline, from, to, date.plusHours(3), date.plusHours(5)));
// ... add more mock flights as needed ...

return flights;
};
}
}
// Step 1: Let's start by defining our list of airlines:

List<String> airlines = Arrays.asList("AirlineA", "AirlineB", "AirlineC");

// Step 2: Setting Up ExecutorService. We need an ExecutorService to manage our threads.
// We'll use a fixed thread pool size for simplicity:

ExecutorService executorService = Executors.newFixedThreadPool(3); // Pool size can be adjusted based on requirements

// Step 3: Implementing the Asynchronous Task Submission Loop.
// Now, let's iterate over the list of airlines and submit each task to the executor service:


List<Future<List<Flight>>> futureFlightsList = new ArrayList<>();
FlightService flightService = new FlightService();

for (String airline : airlines) {
// Assuming we have predefined 'from', 'to', and 'date' variables
Future<List<Flight>> futureFlights = executorService.submit(flightService.fetchFlightsFromAirline(airline, from, to, date));
futureFlightsList.add(futureFlights);
}

// Step 4: Fetching the Results

List<Flight> allFlights = new ArrayList<>();
for (Future<List<Flight>> future : futureFlightsList) {
try {
allFlights.addAll(future.get()); // This call is blocking
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace(); // Handle exceptions as appropriate
}
}

// Step 5: Shutdown the ExecutorService. Don't forget to shut down the executor service to free up resources:

executorService.shutdown();

Complete Example: Putting it all together

import java.util.Arrays;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ExecutionException;
import java.time.LocalDateTime;

public class Main {
public static void main(String[] args) {
List<String> airlines = Arrays.asList("AirlineA", "AirlineB", "AirlineC");
ExecutorService executorService = Executors.newFixedThreadPool(3);
FlightService flightService = new FlightService();

String from = "CityX";
String to = "CityY";
LocalDateTime date = LocalDateTime.now(); // Example date

List<Future<List<Flight>>> futureFlightsList = new ArrayList<>();
for (String airline : airlines) {
futureFlightsList.add(executorService.submit(flightService.fetchFlightsFromAirline(airline, from, to, date)));
}

List<Flight> allFlights = new ArrayList<>();
for (Future<List<Flight>> future : futureFlightsList) {
try {
allFlights.addAll(future.get()); // Blocking call
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
System.out.println(allFlights);

executorService.shutdown();
// Process allFlights as needed
}
}

Output:

  [Flight[airline='AirlineA', from='CityX', to='CityY', departureTime=2024-01-31T20:33:36.881617, arrivalTime=2024-01-31T22:33:36.881617], Flight[airline='AirlineA', from='CityX', to='CityY', departureTime=2024-01-31T23:33:36.881617, arrivalTime=2024-02-01T01:33:36.881617], 
Flight[airline='AirlineB', from='CityX', to='CityY', departureTime=2024-01-31T20:33:36.881617, arrivalTime=2024-01-31T22:33:36.881617], Flight[airline='AirlineB', from='CityX', to='CityY', departureTime=2024-01-31T23:33:36.881617, arrivalTime=2024-02-01T01:33:36.881617], Flight[airline='AirlineC', from='CityX', to='CityY', departureTime=2024-01-31T20:33:36.881617, arrivalTime=2024-01-31T22:33:36.881617], Flight[airline='AirlineC', from='CityX', to='CityY', departureTime=2024-01-31T23:33:36.881617, arrivalTime=2024-02-01T01:33:36.881617]]

Limitations Of Future Interface

The Future interface in Java, while powerful for handling asynchronous operations, does have certain limitations. These constraints can impact how we design and implement concurrency in our Java applications. Here are some of the key limitations:

  1. Blocking Operations: One of the most significant limitations is that the get() method of the Future interface is blocking. It waits until the task is completed, which can lead to inefficiency, especially in scenarios where you need to wait for the completion of multiple Future tasks. This blocking nature can negate some of the benefits of asynchronous programming.
  2. Lack of Direct Support for Completion Callbacks: The Future interface does not provide a built-in mechanism to perform a callback function once the future’s computation is complete. We have to manually check if the task is completed, which is not efficient and does not adhere to a reactive programming model.
  3. No Support for Combining Multiple Futures: Future does not provide native methods to combine multiple futures together or wait for the completion of multiple futures. For example, waiting for the completion of all or any future in a collection of futures requires additional boilerplate code.
  4. Limited Exception Handling: When using the get() method, it can be challenging to handle exceptions elegantly. The method throws ExecutionException if the computation throws an exception, and InterruptedException if the current thread was interrupted while waiting. This requires extra boilerplate code for handling different types of exceptions that may occur during execution.
  5. Inability to Manually Complete a Future: There is no direct way to complete a Future manually or set its value. Once a Future is created and its computation is underway, you cannot modify or update its state.

To address some of these limitations, Java 8 introduced the CompletableFuture class, which provides a more robust and flexible way to handle asynchronous computation. CompletableFuture supports non-blocking operations, callbacks, combining multiple futures, and many more features that overcome the limitations of the traditional Future interface.

Conclusion

The Future interface in Java is a powerful tool for managing asynchronous operations. It allows your applications to perform time-consuming tasks in the background, improving performance and responsiveness. While Future has some limitations, like the inability to manually complete the operation or combine multiple futures, it remains a fundamental part of Java’s concurrency toolkit.

Happy Learning !!!

--

--

Reetesh Kumar

Software developer. Writing about Java, Spring, Cloud and new technologies. LinkedIn: www.linkedin.com/in/reeteshkumar1