1. Overview

With the introduction of lambda expressions in Java 8, it's possible to write code in a more concise and functional way. Streams and Functional Interfaces are the heart of this revolutionary change in the Java platform.

In this quick tutorial, we'll learn whether we should explicitly close Java 8 streams by looking at them from a resource perspective.

2. Closing Streams

Java 8 streams implement the AutoCloseable interface:

public interface Stream<T> extends BaseStream<...> {
    // omitted
}
public interface BaseStream<...> extends AutoCloseable {
    // omitted
}

Put simply, we should think of streams as resources that we can borrow and return when we're done with them. As opposed to most resources, we don't have to always close streams.

This may sound counter-intuitive at first, so let's see when we should and when we shouldn't close Java 8 streams.

2.1. Collections, Arrays, and Generators

Most of the time, we create Stream instances from Java collections, arrays, or generator functions. For instance, here, we're operating on a collection of String via the Stream API:

List<String> colors = List.of("Red", "Blue", "Green")
  .stream()
  .filter(c -> c.length() > 4)
  .map(String::toUpperCase)
  .collect(Collectors.toList());

Sometimes, we're generating a finite or infinite sequential stream:

Random random = new Random();
random.ints().takeWhile(i -> i < 1000).forEach(System.out::println);

Additionally, we can also use array-based streams:

String[] colors = {"Red", "Blue", "Green"};
Arrays.stream(colors).map(String::toUpperCase).toArray()

When dealing with these sorts of streams, we shouldn't close them explicitly. The only valuable resource associated with these streams is memory, and Garbage Collection (GC) takes care of that automatically.

2.2. IO Resources

Some streams, however, are backed by IO resources such as files or sockets. For example, the Files.lines() method streams all lines for the given file:

Files.lines(Paths.get("/path/to/file"))
  .flatMap(line -> Arrays.stream(line.split(",")))
  // omitted

Under the hood, this method opens a FileChannel instance and then closes it upon stream closure. Therefore, if we forget to close the stream, the underlying channel will remain open and then we would end up with a resource leak.

To prevent such resource leaks, it's highly recommended to use the try-with-resources idiom to close IO-based streams:

try (Stream<String> lines = Files.lines(Paths.get("/path/to/file"))) {
    lines.flatMap(line -> Arrays.stream(line.split(","))) // omitted
}

This way, the compiler will close the channel automatically. The key takeaway here is to close all IO-based streams.

Please note that closing an already closed stream would throw IllegalStateException.

3. Conclusion

In this short tutorial, we saw the differences between simple streams and IO-heavy ones. We also learned how those differences inform our decision on whether or not to close Java 8 streams.

As usual, the sample code is available over on GitHub.

Java bottom

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE
Comments are closed on this article!