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

Peter Levart plevart at openjdk.java.net
Sat Nov 6 10:30:40 UTC 2021


On Sun, 3 Oct 2021 11:00:25 GMT, Tagir F. Valeev <tvaleev at openjdk.org> wrote:

> 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
> 
> 
> I suggest to add a new default method to Stream interface named `consumeAndClose`, which allows performing terminal stream operation and closing the stream at the same time. It may look like this:
> 
> 
>     default <R> R consumeAndClose(Function<? super Stream<T>, ? extends R> function) {
>         Objects.requireNonNull(function);
>         try(this) {
>             return function.apply(this);
>         }
>     }
> 
> 
> Now, it will be possible to get the list of the files in the fluent manner:
> 
> 
> List<Path> list = Files.list(Path.of("/etc")).consumeAndClose(Stream::toList);

Gosh, mailing list -> Github bridge didn't handle formatting well, so I'm posting this again via Github. Sorry for duplication...

This RFR is closed, but inspired by John's discussion and fueled by grief that I have each time when I try to combine Stream processing with resources that throw checked exceptions on construction and destruction (usually same kind), I created some helper classes/interfaces that might make such attempts easier and I'd like to present them here to demonstrate what is achievable with API and current syntax features...

Let's start with a code snippet that shows how this must be done without such helper classes today. A method that takes a path and a regular expression and returns a list of lines read from files found recursively below given path that contain at least one match of the regular expression:



    public static List<String> grep(Path dir, Pattern pattern) {
        try (
            var paths = Files.find(dir, 100, (p, a) -> true)
        ) {
            return
                paths
                    .filter(f -> Files.isRegularFile(f) && Files.isReadable(f))
                    .flatMap(
                        f -> {
                            BufferedReader br;
                            try {
                                br = Files.newBufferedReader(f, StandardCharsets.UTF_8);
                            } catch (IOException e) {
                                throw new UncheckedIOException(e);
                            }
                            return br.lines().onClose(() -> {
                                try {
                                    br.close();
                                } catch (IOException e) {
                                    throw new UncheckedIOException(e);
                                }
                            });
                        }
                    )
                    .filter(line -> pattern.matcher(line).find())
                    .collect(Collectors.toList());
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }



This can be rewritten in a more functional style using helpers to:




    public static final Try<BaseStream<?, ?>, IOException> streamTry =
        new Try<>(BaseStream::close, UncheckedIOException::new);

    public static final Try<Closeable, IOException> ioTry =
        new Try<>(Closeable::close, UncheckedIOException::new);

    public static List<String> grep(Path dir, Pattern pattern) {
        return streamTry
            .with(
                () -> Files.find(dir, 100, (p, a) -> true)
            )
            .applyAndDispose(
                paths -> paths
                    .filter(f -> Files.isRegularFile(f) && Files.isReadable(f))
                    .flatMap(
                        f -> ioTry.with(() -> Files.newBufferedReader(f, StandardCharsets.UTF_8))
                                  .apply((br, dispose) -> br.lines().onClose(dispose))
                    )
                    .filter(line -> pattern.matcher(line).find())
                    .collect(Collectors.toList())
            );
    }



Note that this helper is not limited to `AutoCloseable` resources. It captures resource destruction function and checked exception wrapping function in an instance of `Try` which provides two styles of handling resource lifecycle:

`with(constructor).applyAndDispose(consumptor)` - construction, consumprion with automatic resource disposal after the `consumptor` function terminates but before `applyAndDispose` terminates

`with(constructor).apply(consumptorAndDestructor)` - construction, consumption with arranged resource disposal by arranging to call the passed-in `Runnable` instance in the `consumptorAndDestructor` function itself.

Both styles are demonstrated above.

Here's the `Try` helper:

https://gist.github.com/plevart/c26e9908573d4a28c709b7218b001ea8


Regards, Peter

-------------

PR: https://git.openjdk.java.net/jdk/pull/5796


More information about the core-libs-dev mailing list