Discussion: easier Stream closing
Tagir Valeev
amaembo at gmail.com
Sun Sep 26 09:27:58 UTC 2021
Hello!
With current NIO file API, a very simple problem to get the list of
all files in the directory requires some ceremony:
List<Path> paths;
try (Stream<Path> stream = Files.list(Path.of("/etc"))) {
paths = stream.toList();
}
If we skip try-with-resources, we may experience OS file handles leak,
so it's desired to keep it. Yet, it requires doing boring stuff. In
this sense, classic new File("/etc").list() was somewhat more
convenient (despite its awful error handling).
I like how this problem is solved in StackWalker API [1]: it limits
the lifetime of the Stream by requiring a user-specified function to
consume it. After the function is applied, the stream is closed
automatically. We could add a similar overload to the Files API. As an
additional feature, we could also translate all UncheckedIOException's
back to IOException for more uniform exception processing:
/**
* @param dir The path to the directory
* @param function function to apply to the stream of directory files
* @param <T> type of the result
* @return result of the function
* @throws IOException if an I/O error occurs when opening the directory, or
* UncheckedIOException is thrown by the supplied function.
*/
public static <T> T list(Path dir, Function<? super Stream<Path>, ?
extends T> function) throws IOException {
try (Stream<Path> stream = Files.list(dir)) {
return function.apply(stream);
}
catch (UncheckedIOException exception) {
throw exception.getCause();
}
}
In this case, getting the List of all files in the directory will be
as simple as
List<Path> paths = Files.list(Path.of("/etc"), Stream::toList);
This doesn't limit the flexibility. For example, if we need only file
names instead of full paths, we can do this:
List<Path> paths = Files.list(Path.of("/etc"), s ->
s.map(Path::getFileName).toList());
Alternatively, we could enhance the BaseStream interface in a similar
way. It won't allow us to translate exceptions, but could be useful
for every stream that must be closed after consumption:
// in BaseStream:
/**
* Apply a given function to this stream, then close the stream.
* No further operation on the stream will be possible after that.
*
* @param function function to apply
* @param <R> type of the function result
* @return result of the function
* @see #close()
*/
default <R> R consumeAndClose(Function<? super S, ? extends R> function) {
try(@SuppressWarnings("unchecked") S s = (S) this) {
return function.apply(s);
}
}
The usage would be a little bit longer but still more pleasant than
explicit try-with-resources:
List<Path> list = Files.list(Path.of("/etc")).consumeAndClose(Stream::toList);
On the other hand, in this case, we are free to put intermediate
operations outside of consumeAndClose, so we won't need to nest
everything inside the function. Only terminal operation should be
placed inside the consumeAndClose. E.g., if we need file names only,
like above, we can do this:
List<Path> list =
Files.list(Path.of("/etc")).map(Path::getFileName).consumeAndClose(Stream::toList);
What do you think?
[1] https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/StackWalker.html#walk(java.util.function.Function)
More information about the core-libs-dev
mailing list