Uploaded image for project: 'JDK'
  1. JDK
  2. JDK-8274994

Add a method to Stream API to consume and close the stream without using try-with-resources

XMLWordPrintable

    • Icon: CSR CSR
    • Resolution: Unresolved
    • Icon: P4 P4
    • None
    • core-libs
    • None
    • minimal
    • Hide
      Compatibility risk may occur if non-JDK code extends or implements *Stream interfaces and adds a method with the same name `consumeAndClose` that has an incompatible signature. As the proposed method name is quite long and specific, the change of such collision is extremely low. Google search yields two opensource JVM classes containing the method with the same name:

      1. net.sf.asyncobjects.asyncscala.stream.RichStream (Scala API), despite the similar name, it's not Java Stream inheritor
      http://asyncobjects.sourceforge.net/asyncscala/asyncscala-core/scaladocs/net/sf/asyncobjects/asyncscala/stream/RichStream.html

      2. org.stagemonitor.util.IOUtils - does not inherit Stream, the consumeAndClose method is static
      https://www.mvndoc.com/c/org.stagemonitor/stagemonitor-configuration/org/stagemonitor/util/IOUtils.html#consumeAndClose-java.io.InputStream-
      Show
      Compatibility risk may occur if non-JDK code extends or implements *Stream interfaces and adds a method with the same name `consumeAndClose` that has an incompatible signature. As the proposed method name is quite long and specific, the change of such collision is extremely low. Google search yields two opensource JVM classes containing the method with the same name: 1. net.sf.asyncobjects.asyncscala.stream.RichStream (Scala API), despite the similar name, it's not Java Stream inheritor http://asyncobjects.sourceforge.net/asyncscala/asyncscala-core/scaladocs/net/sf/asyncobjects/asyncscala/stream/RichStream.html 2. org.stagemonitor.util.IOUtils - does not inherit Stream, the consumeAndClose method is static https://www.mvndoc.com/c/org.stagemonitor/stagemonitor-configuration/org/stagemonitor/util/IOUtils.html#consumeAndClose-java.io.InputStream-
    • Java API
    • JDK

      Summary

      It's proposed to add new default method named consumeAndClose to the following interfaces:

      • java.util.stream.Stream
      • java.util.stream.IntStream
      • java.util.stream.LongStream
      • java.util.stream.DoubleStream

      Problem

      Currently, when the stream holds a resource, it's necessary to wrap it with try-with-resources. This undermines the compact and fluent style of stream API calls. For example, if we want to get the List of files inside the directory and timely close the underlying filehandle, we should use something like this:

      List<Path> paths;
      try (Stream<Path> stream = Files.list(Path.of("/etc"))) {
          paths = stream.toList();
      }
      // use paths

      Try-with-resources construct is heavy and doesn't align well with the usual fluent Stream API style. As a result, people often forget to close the stream that holds a resource and needs to be closed. This may cause file descriptors to leak.

      Solution

      A proposed solution: add the consumeAndClose method to the stream interfaces. This method accepts a function that accepts a stream and performs a terminal stream operation returning the result. The function is executed and the original stream is closed after that (even if the function execution failed). Having this method, the code snippet above could be rewritten as:

      List<Path> list = Files.list(Path.of("/etc")).consumeAndClose(Stream::toList);

      Now, the stream could be processed as a single expression which is more in the Stream API spirit.

      Alternative solutions:

      1. Instead of adding four methods to all the stream specializations, it's possible to add only one method to the super-interface java.util.stream.BaseStream. However, Brian Goetz was against this.

      2. It's possible to avoid changes in the Stream interface and provide overloads for methods producing the stream that holds the resource. For example, we may accompany Files.list(Path dir) with File list(Path dir, Function<? super Stream<Path>, ? extends T> function) overload. The advantage of this approach is that overloads are easier to discover than completely new method. Also, here it's possible to add exception translation (e.g., unwrap UncheckedIOException, so only checked IOException is thrown in case of I/O error). The disadvantage is that it requires adding one method per every existing method that holds a resource which could be too many methods. Sometimes, methods already have overloads. E.g., there is Files.lines(Path) and Files.lines(Path, Charset), so we will need two more overloads making the API somewhat bloated and confusing. Also, some existing methods use variable arity parameter (e.g., Files.walk(Path, FileVisitOption...)), so it will be not possible to put a new lambda argument at the end which is again confusing.

      3. It's possible to revisit the old decision and automatically close the consumed streams after the terminal operation is executed. However, this won't work with iterator() and spliterator() terminal operations, as they still need the resource to be opened and there's no way to indicate that iterator() is not necessary anymore (it could be legally dropped in the middle of traversal). Also, this introduces quite a major behavioral change. For background, here's a StackOverflow discussion about this topic.

      Specification

      The following are the added methods, along with the proposed specification.

      // java.util.stream.Stream
          /**
           * Applies given function to this stream, then closes the stream.
           * No further operation on the stream will be possible after that.
           *
           * @apiNote
           * This method allows consuming and closing the stream in the single
           * operation, making it possible to perform the operation on the stream
           * holding a resource in a single statement. For example, it's possible to write:
           *
           * <pre>{@code
           * List<Path> paths = Files.list(Path.of(".")).consumeAndClose(Stream::toList);
           * }</pre>
           *
           * This is equivalent to:
           *
           * <pre>{@code
           * try (Stream<Path> stream = Files.list(Path.of("."))) {
           *   List<Path> paths = stream.consumeAndClose(Stream::toList);
           * }}</pre>
           *
           * @param function function to apply
           * @param <R>      type of the function result
           * @return result of the function
           * @throws NullPointerException if the supplied function is null
           * @see #close()
           * @since 18
           */
          default <R> R consumeAndClose(Function<? super Stream<T>, ? extends R> function) { ... }
      
      // java.util.stream.IntStream
          /**
           * Applies given function to this stream, then closes the stream.
           * No further operation on the stream will be possible after that.
           *
           * @apiNote
           * This method allows consuming and closing the stream in the single
           * operation, making it possible to perform the operation on the stream
           * holding a resource in a single statement.
           *
           * @param function function to apply
           * @param <R>      type of the function result
           * @return result of the function
           * @throws NullPointerException if the supplied function is null
           * @see #close()
           * @since 18
           */
          default <R> R consumeAndClose(Function<? super IntStream, ? extends R> function) { ... }
      
      // java.util.stream.LongStream
          /**
           * Applies given function to this stream, then closes the stream.
           * No further operation on the stream will be possible after that.
           *
           * @apiNote
           * This method allows consuming and closing the stream in the single
           * operation, making it possible to perform the operation on the stream
           * holding a resource in a single statement.
           *
           * @param function function to apply
           * @param <R>      type of the function result
           * @return result of the function
           * @throws NullPointerException if the supplied function is null
           * @see #close()
           * @since 18
           */
          default <R> R consumeAndClose(Function<? super LongStream, ? extends R> function) { ... }
      
      // java.util.stream.DoubleStream
          /**
           * Applies given function to this stream, then closes the stream.
           * No further operation on the stream will be possible after that.
           *
           * @apiNote
           * This method allows consuming and closing the stream in the single
           * operation, making it possible to perform the operation on the stream
           * holding a resource in a single statement.
           *
           * @param function function to apply
           * @param <R>      type of the function result
           * @return result of the function
           * @throws NullPointerException if the supplied function is null
           * @see #close()
           * @since 18
           */
          default <R> R consumeAndClose(Function<? super DoubleStream, ? extends R> function) { ... }

            tvaleev Tagir Valeev
            tvaleev Tagir Valeev
            Votes:
            0 Vote for this issue
            Watchers:
            0 Start watching this issue

              Created:
              Updated: