简体   繁体   中英

Java 8 Streams and try with resources

I thought that the stream API was here to make the code easier to read. I found something quite annoying. The Stream interface extends the java.lang.AutoCloseable interface.

So if you want to correctly close your streams, you have to use try with resources.

Listing 1 . Not very nice, streams are not closed.

public void noTryWithResource() {
    Set<Integer> photos = new HashSet<Integer>(Arrays.asList(1, 2, 3));

    @SuppressWarnings("resource") List<ImageView> collect = photos.stream()
        .map(photo -> new ImageView(new Image(String.valueOf(photo))))
        .collect(Collectors.<ImageView>toList());
}

Listing 2 . With 2 nested try

public void tryWithResource() {
    Set<Integer> photos = new HashSet<Integer>(Arrays.asList(1, 2, 3));

    try (Stream<Integer> stream = photos.stream()) {
        try (Stream<ImageView> map = stream
                .map(photo -> new ImageView(new Image(String.valueOf(photo)))))
        {
            List<ImageView> collect = map.collect(Collectors.<ImageView>toList());
        }
    }
}

Listing 3 . As map returns a stream, both the stream() and the map() functions have to be closed.

public void tryWithResource2() {
    Set<Integer> photos = new HashSet<Integer>(Arrays.asList(1, 2, 3));

    try (Stream<Integer> stream = photos.stream(); Stream<ImageView> map = stream.map(photo -> new ImageView(new Image(String.valueOf(photo)))))
    {
        List<ImageView> collect = map.collect(Collectors.<ImageView>toList());
    }
}

The example I give does not make any sense. I replaced Path to jpg images with Integer , for the sake of the example. But don't let you distract by these details.

What is the best way to go around with those auto closable streams. I have to say I'm not satisfied with any of the 3 options I showed. What do you think? Are there yet other more elegant solutions?

You're using @SuppressWarnings("resource") which presumably suppresses a warning about an unclosed resource. This isn't one of the warnings emitted by javac . Web searches seem to indicate that Eclipse issues warnings if an AutoCloseable is left unclosed.

This is a reasonable warning according to the Java 7 specification that introduced AutoCloseable :

A resource that must be closed when it is no longer needed.

However, the Java 8 specification for AutoCloseable was relaxed to remove the "must be closed" clause. It now says, in part,

An object that may hold resources ... until it is closed.

It is possible, and in fact common, for a base class to implement AutoCloseable even though not all of its subclasses or instances will hold releasable resources. For code that must operate in complete generality, or when it is known that the AutoCloseable instance requires resource release, it is recommended to use try-with-resources constructions. However, when using facilities such as Stream that support both I/O-based and non-I/O-based forms, try-with-resources blocks are in general unnecessary when using non-I/O-based forms.

This issue was discussed extensively within the Lambda expert group; this message summarizes the decision. Among other things it mentions changes to the AutoCloseable specification (cited above) and the BaseStream specification (cited by other answers). It also mentions the possible need to adjust the Eclipse code inspector for the changed semantics, presumably not to emit warnings unconditionally for AutoCloseable objects. Apparently this message didn't get to the Eclipse folks or they haven't changed it yet.

In summary, if Eclipse warnings are leading you into thinking that you need to close all AutoCloseable objects, that's incorrect. Only certain specific AutoCloseable objects need to be closed. Eclipse needs to be fixed (if it hasn't already) not to emit warnings for all AutoCloseable objects.

You only need to close Streams if the stream needs to do any cleanup of itself, usually I/O. Your example uses an HashSet so it doesn't need to be closed.

from the Stream javadoc:

Generally, only streams whose source is an IO channel (such as those returned by Files.lines(Path, Charset)) will require closing. Most streams are backed by collections, arrays, or generating functions, which require no special resource management.

So in your example this should work without issue

List<ImageView> collect = photos.stream()
                       .map(photo -> ...)
                       .collect(toList());

EDIT

Even if you need to clean up resources, you should be able to use just one try-with-resource. Let's pretend you are reading a file where each line in the file is a path to an image:

 try(Stream<String> lines = Files.lines(file)){
       List<ImageView> collect = lines
                                  .map(line -> new ImageView( ImageIO.read(new File(line)))
                                  .collect(toList());
  }

“Closeable” means “can be closed”, not “must be closed”.

That was true in the past, eg see ByteArrayOutputStream :

Closing a ByteArrayOutputStream has no effect.

And that is true now for Stream s where the documentation makes clear :

Streams have a BaseStream.close() method and implement AutoCloseable , but nearly all stream instances do not actually need to be closed after use. Generally, only streams whose source is an IO channel (such as those returned by Files.lines(Path, Charset) ) will require closing.

So if an audit tool generates false warnings, it's a problem of the audit tool, not of the API.

Note that even if you want to add resource management, there is no need to nest try statements. While the following is sufficient:

final Path p = Paths.get(System.getProperty("java.home"), "COPYRIGHT");
try(Stream<String> stream=Files.lines(p, StandardCharsets.ISO_8859_1)) {
    System.out.println(stream.filter(s->s.contains("Oracle")).count());
}

you may also add the secondary Stream to the resource management without an additional try :

final Path p = Paths.get(System.getProperty("java.home"), "COPYRIGHT");
try(Stream<String> stream=Files.lines(p, StandardCharsets.ISO_8859_1);
    Stream<String> filtered=stream.filter(s->s.contains("Oracle"))) {
    System.out.println(filtered.count());
}

It is possible to create a utility method that reliably closes streams with a try-with-resource-statement.

It is a bit like a try-finally that is an expression (something that is the case in eg Scala).

/**
 * Applies a function to a resource and closes it afterwards.
 * @param sup Supplier of the resource that should be closed
 * @param op operation that should be performed on the resource before it is closed
 * @return The result of calling op.apply on the resource 
 */
private static <A extends AutoCloseable, B> B applyAndClose(Callable<A> sup, Function<A, B> op) {
    try (A res = sup.call()) {
        return op.apply(res);
    } catch (RuntimeException exc) {
        throw exc;
    } catch (Exception exc) {
        throw new RuntimeException("Wrapped in applyAndClose", exc);
    }
}

(Since resources that need to be closed often also throw exceptions when they are allocated non-runtime exceptions are wrapped in runtime exceptions, avoiding the need for a separate method that does that.)

With this method the example from the question looks like this:

Set<Integer> photos = new HashSet<Integer>(Arrays.asList(1, 2, 3));

List<ImageView> collect = applyAndClose(photos::stream, s -> s
    .map(photo -> new ImageView(new Image(String.valueOf(photo))))
    .collect(Collectors.toList()));

This is useful in situations when closing the stream is required, such as when using Files.lines . It also helps when you have to do a "double close", as in your example in Listing 3 .

This answer is an adaptation of an old answer to a similar question.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM