Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Calling different methods based on values of two Optionals

While working with Java 8 Optionals I face following scenario very frequently. I have two Optional objects and then I want to call different methods based on the values (ifPresent) of those Optionals.

Here is an example:

void example(Optional<String> o1, Optional<String> o2) throws Exception {
    if (o1.isPresent() && o2.isPresent()) {
       handler1(o1.get(), o2.get());
    } else if (o1.isPresent()) {
       handler2(o1.get());
    } else if (o2.isPresent()) {
       handler3(o2.get());
    } else {
       throw new Exception();
    }
}

However, this chain of if-else statements doesn't seem like a proper way of working with Optional (after all, they were added so that you can avoid writing these if-else checks everywhere in your code).

What is the proper way of doing this with Optional objects?

like image 268
Nullpointer Avatar asked Aug 14 '18 18:08

Nullpointer


People also ask

What is optional method?

Optional is a container object used to contain not-null objects. Optional object is used to represent null with absent value. This class has various utility methods to facilitate code to handle values as 'available' or 'not available' instead of checking null values.

Why does Java 8 have optionals?

So, to overcome this, Java 8 has introduced a new class Optional in java. util package. It can help in writing a neat code without using too many null checks. By using Optional, we can specify alternate values to return or alternate code to run.

Which of the following methods of optional class can be used to throw an exception of your choice if an object is null?

orElseThrow Family Similar to the orElse method, the Optional class provides an orElseThrow method that allows us to throw an exception when obtaining the wrapped value if the Optional is empty.


2 Answers

You said that you use such structure frequently, so I propose to introduce a Helper class:

final class BiOptionalHelper<F, S> {
    private final Optional<F> first;
    private final Optional<S> second;

    public BiOptionalHelper(Optional<F> first, Optional<S> second){
        this.first = first;
        this.second = second;
    }

    public BiOptionalHelper<F, S> ifFirstPresent(Consumer<? super F> ifPresent){
        if (!second.isPresent()) {
            first.ifPresent(ifPresent);
        }
        return this;
    }

    public BiOptionalHelper<F, S> ifSecondPresent(Consumer<? super S> ifPresent){
        if (!first.isPresent()) {
            second.ifPresent(ifPresent);
        }
        return this;
    }

    public BiOptionalHelper<F, S> ifBothPresent(BiConsumer<? super F, ? super S> ifPresent){
        if(first.isPresent() && second.isPresent()){
            ifPresent.accept(first.get(), second.get());
        }
        return this;
    }

    public <T extends Throwable> void orElseThrow(Supplier<? extends T> exProvider) throws T{
        if(!first.isPresent() && !second.isPresent()){
            throw exProvider.get();
        }
    }
}

Which then may be used in a way like this:

new BiOptionalHelper<>(o1, o2)
    .ifBothPresent(this::handler1)
    .ifFirstPresent(this::handler2)
    .ifSecondPresent(this::handler3)
    .orElseThrow(Exception::new);

Though, this just moves your problem into a separate class.

Note: above code may be refactored to not use Optional and isPresent() checks at all. And just use null for first and second and replace isPresent() with null-checks.

As it is generally a bad design to store Optional in fields or accept them as parameters in the first place. As JB Nizet already pointed out in a comment to the question.


Another way it to move that logic into common helper method:

public static <F, S, T extends Throwable> void handle(Optional<F> first, Optional<S> second, 
                                                      BiConsumer<F, S> bothPresent, Consumer<F> firstPresent, 
                                                      Consumer<S> secondPresent, Supplier<T> provider) throws T{
    if(first.isPresent() && second.isPresent()){
        bothPresent.accept(first.get(), second.get());
    } else if(first.isPresent()){
        firstPresent.accept(first.get());
    } else if(second.isPresent()){
        secondPresent.accept(second.get());
    } else{
        throw provider.get();
    }
}

Which then could be called like this:

handle(o1, o2, this::handler1, this::handler2, this::handler3, Exception::new);

But it's still kind of messy to be honest.

like image 86
Lino Avatar answered Sep 27 '22 23:09

Lino


Disclaimer: My answer is based on Lino's answer - the first part of this answer (BiOptional<T, U>) is a modified version of Lino's BiOptionalHelper, while the second part (BiOptionalMapper<T, U, R>) is my idea for extending this nice pattern.

I like Lino's answer a lot. However, I feel that instead of calling it BiOptionalHelper, it deserves to be simply called BiOptional, provided that:

  • it gets Optional<T> first() and Optional<T> second() methods
  • it gets is(First/Second)Present, is(First/Second)OnlyPresent and are(Both/None)Present methods
  • if(First/Second)Present methods are renamed to if(First/Second)OnlyPresent
  • it gets ifNonePresent(Runnable action) method
  • orElseThrow method is renamed to ifNonePresentThrow

Finally (and this is the entirely original part of my answer), I realized this pattern could support not only "handling" (in BiOptional), but also "mapping" (in BiOptionalMapper obtained through BiOptional.mapper()), like that:

BiOptional<String, Integer> biOptional = BiOptional.from(o1, o2);

// handler version
biOptional
        .ifBothPresent(this::handleBoth)
        .ifFirstOnlyPresent(this::handleFirst)
        .ifSecondOnlyPresent(this::handleSecond)
        .ifNonePresent(this::performAction);

// mapper version
String result = biOptional.<String>mapper()
        .onBothPresent(this::mapBoth)
        .onFirstOnlyPresent(this::mapFirst)
        .onSecondOnlyPresent(this::mapSecond)
        .onNonePresent("default")
        .result();

Optional<String> optionalResult = biOptional.<String>mapper()
        .onBothPresent(this::mapBoth)
        .onNonePresentThrow(IllegalStateException::new)
        .optionalResult();

Note that one can either:

  • call all on*Present mapping methods, and then call R result() (which will throw if result were to be absent), or
  • call only some of them, and then call Optional<R> optionalResult()

Note also that:

  • in order to avoid confusion between "handling" and "mapping", the naming convention is as follows:
    • BiOptional: if*Present
    • BiOptionalMapper: on*Present
  • if any of the on*Present methods is called twice, BiOptionalMapper will throw if result were to be overwritten (unlike BiOptional, which can handle multiple if*Present calls)
  • result cannot be set to null by the mappers provided to on*Present or by calling onNonePresent(R) (Optional<...> should be used as result type R instead)

Here's the source code of the two classes:

final class BiOptional<T, U> {

    @Nullable
    private final T first;
    @Nullable
    private final U second;

    public BiOptional(T first, U second) {
        this.first = first;
        this.second = second;
    }

    public static <T, U> BiOptional<T, U> from(Optional<T> first, Optional<U> second) {
        return new BiOptional<>(first.orElse(null), second.orElse(null));
    }

    public Optional<T> first() {
        return Optional.ofNullable(first);
    }

    public Optional<U> second() {
        return Optional.ofNullable(second);
    }

    public boolean isFirstPresent() {
        return first != null;
    }

    public boolean isSecondPresent() {
        return second != null;
    }

    public boolean isFirstOnlyPresent() {
        return isFirstPresent() && !isSecondPresent();
    }

    public boolean isSecondOnlyPresent() {
        return !isFirstPresent() && isSecondPresent();
    }

    public boolean areBothPresent() {
        return isFirstPresent() && isSecondPresent();
    }

    public boolean areNonePresent() {
        return !isFirstPresent() && !isSecondPresent();
    }

    public BiOptional<T, U> ifFirstOnlyPresent(Consumer<? super T> ifFirstOnlyPresent) {
        if (isFirstOnlyPresent()) {
            ifFirstOnlyPresent.accept(first);
        }
        return this;
    }

    public BiOptional<T, U> ifSecondOnlyPresent(Consumer<? super U> ifSecondOnlyPresent) {
        if (isSecondOnlyPresent()) {
            ifSecondOnlyPresent.accept(second);
        }
        return this;
    }

    public BiOptional<T, U> ifBothPresent(BiConsumer<? super T, ? super U> ifBothPresent) {
        if (areBothPresent()) {
            ifBothPresent.accept(first, second);
        }
        return this;
    }

    public BiOptional<T, U> ifNonePresent(Runnable ifNonePresent) {
        if (areNonePresent()) {
            ifNonePresent.run();
        }
        return this;
    }

    public <X extends Throwable> void ifNonePresentThrow(Supplier<? extends X> throwableProvider) throws X {
        if (areNonePresent()) {
            throw throwableProvider.get();
        }
    }

    public <R> BiOptionalMapper<T, U, R> mapper() {
        return new BiOptionalMapper<>(this);
    }
}

and:

final class BiOptionalMapper<T, U, R> {

    private final BiOptional<T, U> biOptional;
    private R result = null;

    BiOptionalMapper(BiOptional<T, U> biOptional) {
        this.biOptional = biOptional;
    }

    public BiOptionalMapper<T, U, R> onFirstOnlyPresent(Function<? super T, ? extends R> firstMapper) {
        if (biOptional.isFirstOnlyPresent()) {
            setResult(firstMapper.apply(biOptional.first().get()));
        }
        return this;
    }

    public BiOptionalMapper<T, U, R> onSecondOnlyPresent(Function<? super U, ? extends R> secondMapper) {
        if (biOptional.isSecondOnlyPresent()) {
            setResult(secondMapper.apply(biOptional.second().get()));
        }
        return this;
    }

    public BiOptionalMapper<T, U, R> onBothPresent(BiFunction<? super T, ? super U, ? extends R> bothMapper) {
        if (biOptional.areBothPresent()) {
            setResult(bothMapper.apply(biOptional.first().get(), biOptional.second().get()));
        }
        return this;
    }

    public BiOptionalMapper<T, U, R> onNonePresent(Supplier<? extends R> supplier) {
        if (biOptional.areNonePresent()) {
            setResult(supplier.get());
        }
        return this;
    }

    public BiOptionalMapper<T, U, R> onNonePresent(R other) {
        if (biOptional.areNonePresent()) {
            setResult(other);
        }
        return this;
    }

    public <X extends Throwable> BiOptionalMapper<T, U, R> onNonePresentThrow(Supplier<? extends X> throwableProvider) throws X {
        biOptional.ifNonePresentThrow(throwableProvider);
        return this;
    }

    public R result() {
        if (result == null) {
            throw new IllegalStateException("Result absent");
        }
        return result;
    }

    public Optional<R> optionalResult() {
        return Optional.ofNullable(result);
    }

    private void setResult(R result) {
        if (result == null) {
            throw new IllegalArgumentException("Null obtained from a mapper");
        }
        if (this.result != null) {
            throw new IllegalStateException("Result already present: " + this.result);
        }
        this.result = result;
    }
}
like image 38
Tomasz Linkowski Avatar answered Sep 27 '22 23:09

Tomasz Linkowski