8

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!

2 Answers 2

16

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/).

Sign up to request clarification or add additional context in comments.

Comments

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);
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.