Using Optional with Streams in Java
Solution 1
One of the nice things about Optionals is that applying filtering, mapping and flat-mapping functions on them only trigger when Optional::isPresent is true, so:
public void run() throws IOException {
Files.list(source)
.filter(Images::isImage)
.map(this::createImage)
// turns every non-proportional Optional<Image> into empty optionals
.map(image -> image.filter(i -> i.isProportional(this.height, this.width)))
// resizes every proportional Optional<Image>, while doing nothing on the empties
.map(image -> image.map(i -> i.resize(this.height, this.width)))
// applies the writing consumer for each non-empty Optional<Image>
.forEach(image -> image.ifPresent(this::write));
}
Another way is to only call Optional::isPresent and Optional::get in separate Stream transformations:
public void run() throws IOException {
Files.list(source)
.filter(Images::isImage)
.map(this::createImage)
// filter out the empty optionals
.filter(Optional::isPresent)
// replace every optional with its contained value
.map(Optional::get)
.filter(image -> image.isProportional(this.height, this.width))
.map(image -> image.resize(this.height, this.width))
.forEach(this::write);
}
Yet another way (which I refuse to recommend as a primary solution because of its relative weirdness) is to change the static image creation method into a Stream generator, instead of an Optional generator, to take advantage of flatMap:
private Stream<Image> createImage(Path imagePath) {
try {
return Stream.of(new Image(imagePath));
} catch (IOException e) {
return Stream.empty();
}
}
public void run() throws IOException {
Files.list(source)
.filter(Images::isImage)
// inserts into the stream the resulting image (empty streams are handled seamlessly)
.flatMap(this::createImage)
.filter(image -> image.isProportional(this.height, this.width))
.map(image -> image.resize(this.height, this.width))
.forEach(this::write);
}
On second thought, go with this solution; it seems to be simpler, and since the static method is private anyway, less screaming would occur from end-users, other developers, and random people with access to decent Java 8 decompilers (http://www.benf.org/other/cfr/).
Solution 2
From Java9, you can use flatMap
and Optional::stream
to filter empty Optionals:
public void run() throws IOException {
Files.list(source)
.filter(Images::isImage)
.map(this::createImage)
.flatMap(Optional::stream)
.filter(image -> image.isProportional(this.height, this.width))
.map(image -> image.resize(this.height, this.width))
.forEach(this::write);
}
gaijinco
Updated on June 17, 2022Comments
-
gaijinco almost 2 years
I was trying to refactor an old code to use streams, and my first approach was this:
public void run() throws IOException { Files.list(this.source) .filter(Images::isImage) .map(Image::new) .filter(image -> image.isProportional(this.height, this.width)) .map(image -> image.resize(this.height, this.width)) .forEach(image -> Images.write(image, this.destination)); }
This is not compiling since new Image() and Images.write() throws IOExceptions.
Wrapping those exceptions with UncheckedIOException wouldn't do the trick as I don't want to stop other images to be processed if one of them fails.
So I ended writing 2 private methods:
private Optional<Image> createImage(Path imagePath) { try { return Optional.of(new Image(imagePath)); } catch (IOException e) { return Optional.empty(); } } private void write(Image image) { try { Images.write(image, this.destination); } catch (IOException e) { // log error } }
createImage() returns an Optional since this seems sensible. However after this my code got really ugly:
public void run() throws IOException { Files.list(source) .filter(Images::isImage) .map(this::createImage) .filter(image -> image.isPresent() && image.get().isProportional(this.height, this.width)) .map(image -> image.get().resize(this.height, this.width)) .forEach(this::write); }
Is there a way to avoid using get() and isPresent() on that code?
Thanks!