Using Optional with Streams in Java

13,132

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);
}
Share:
13,132
gaijinco
Author by

gaijinco

Updated on June 17, 2022

Comments

  • gaijinco
    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!