11

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?

Nullpointer
  • 1,086
  • 7
  • 20
  • 6
    Optionals were not added to avoid if-else checks. They were added to make it clear that a method can return nothing instead of what it usually returns, in a way that forces the caller to handle the nothing case. If you use them to avoid if/else checks, you're abusing them. – JB Nizet Aug 14 '18 at 19:00
  • 1
    Have you seen: ? – piola Aug 14 '18 at 19:09
  • A slightly more compact version could be: `void example(Optional o1, Optional o2) throws Exception { o1.ifPresent(val1 -> o2.ifPresent(val2 -> handler1(val1, val2))); o1.ifPresent(val1 -> { if (!o2.isPresent()) handler2(val1); }); o2.ifPresent(val2 -> { if (!o1.isPresent()) handler3(val2); }); if (!o1.isPresent() && !o2.isPresent()) throw new Exception(); }` With this you get automatic unwrapping of the values (no need for `get`). Still ugly, though. You could get rid of the final line, if throwing an exception is not necessary. – Janus Varmarken Aug 14 '18 at 19:57

4 Answers4

6

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.

Lino
  • 19,604
  • 6
  • 47
  • 65
  • This `BiOptionalHelper` is really nice! The `handle` method, on the other hand, is really bad and unintuitive, like you admitted yourself ;) One note: I'd replace `first.ifPresent(s1 -> second.ifPresent(s2 -> ifPresent.accept(s1, s2)));` with much clearer `if (first.isPresent() && second.isPresent()) { ifPresent.accept(first.get(), second.get(); }` (it'd even match the body of `orElseThrow` then). – Tomasz Linkowski Aug 14 '18 at 20:08
  • Doesn't the `BiOptionalHelper` end up invoking both `handler1`, `handler2`, as well as `handler3`, when both values are present, or am I missing something? OP only wants to invoke `handler2` or `handler3` when only one value is present. – Janus Varmarken Aug 14 '18 at 20:17
  • @JanusVarmarken completly right, I didn't notice that one, will need to come up with something different. One would probably need to instead of `return` this, return something that has already been executed – Lino Aug 14 '18 at 20:21
  • 1
    @TomaszLinkowski not really... as it does not do what the OP wants, read previous comments – Eugene Aug 14 '18 at 20:24
  • @Lino but in that case you would not be able to chain those methods – Eugene Aug 14 '18 at 20:25
  • @Lino I think you can fix it by inserting the `if` checks I use in the comment I made to the question text in your `ifFirstPresent` and `ifSecondPresent`. – Janus Varmarken Aug 14 '18 at 20:25
  • @Eugene maybe use an internal *executed* flag with a second private constructor, which initially is set to `false` – Lino Aug 14 '18 at 20:29
  • I'll fiddle around a bit more and try to come up with a better solution – Lino Aug 14 '18 at 20:37
  • @Lino it would have to be an array of a single boolean btw, since you would need to update it from within a lambda expression – Eugene Aug 14 '18 at 20:39
  • 1
    Suggested an edit that I believe solves it, but it essentially just moves what the OP is doing in his current code to your helper class – Janus Varmarken Aug 14 '18 at 20:40
  • 1
    @JanusVarmarken well, may be that would be acceptable by the OP, I actually don't like all this overhead that is being done here, and would go for the helper method, for which +1 (with the additional fixes suggested) – Eugene Aug 14 '18 at 20:43
  • 1
    @Lino You lost me, sorry. I just suggested to wrap the invocations of `ifPresent` `Consumer`s in an if-check, e.g., `public BiOptionalHelper ifFirstPresent(Consumer super F> ifPresent){ if (!second.isPresent()) { first.ifPresent(ifPresent); } return this; }` I don't believe there's a need for keeping track of state if you do this. – Janus Varmarken Aug 14 '18 at 20:57
  • 1
    @Lino Thanks for taking effort of writing this class. It is certainly helpful. I can not use it in my current codebase. But I think it is a good addition if you are working with `Optional`s a lot! – Nullpointer Aug 14 '18 at 21:20
  • @Nullpointer You are welcome, and that's sad to hear that. As you may have seen and read across all the other answers and comments: it is probably the best, to just stick with the code you currently have, or use the helper method in the second part of my answer – Lino Aug 14 '18 at 21:23
  • @Eugene @Lino Please, do not suggest using this helper method to people. Would you guys like to work with code that uses it? ;) I mean, it's totally unintuitive when you see it used, especially when both `Optional`s are of the same type. Until Java has [named parameters](https://en.wikipedia.org/wiki/Named_parameter), I believe such methods should be avoided. As to the overhead related to using the helper class, it's entirely optimized out by the JIT and its [escape analysis](https://en.wikipedia.org/wiki/Escape_analysis#Optimizations) (meaning the object is not even allocated on the heap). – Tomasz Linkowski Aug 15 '18 at 11:19
  • @TomaszLinkowski that object not being allocated on the heap is called scalarization. And I actually like that method and yes I used use it. I've looked at your code and it expresses the intention a lot better, agreed, but the amount of code... – Eugene Aug 15 '18 at 11:39
  • @Eugene Well, I generally like your code here on SO, but this method - it's scary :P As to amount of code in `BiOptionalHelper`: it's not business logic, it's utility code, like `ReferencePipeline` implementing `Stream`. So their internals do not have to be that readable until their contracts are clear & intuitive. So I'd say that: 1) the original OP's code is quite clear (if a little verbose), 2) `BiOptionalHelper` usage may look a bit strange at first, but at least it's clear about what it does, 3) the **6**-parameter helper method, though - this can *really* obscure the business logic. – Tomasz Linkowski Aug 15 '18 at 11:58
  • @TomaszLinkowski comment coming on your answer soon... plz give me some time – Eugene Aug 15 '18 at 11:59
6

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;
    }
}
Tomasz Linkowski
  • 4,386
  • 23
  • 38
  • 3
    1+ for the effort in this one, it clearly denotes the intention, a few things I don't like : `isFirstPresent` and the like are implementation details, I would make them private; I would remove `first/second` methods entirely (since this is not what this utility is for IMO); I would leave `areBothPresent` only and simply do `!areBothPresent()` when needed. More coming after the short review we will have internally on this, apparently my team lead loves it and says move it to the `xxx.HolgerUtil` package :) so... I guess I'm wrong, which is the main reason I'm here to begin with(read "NICE!") – Eugene Aug 15 '18 at 12:13
  • I agree with @Eugene, `BiOptional` is not used to store the `Optional`s, but just to handle them, which leaves all those helper methods unused and useless. – Lino Aug 15 '18 at 12:22
  • Also I made once a true `BiOptional`, you can see it here: https://ideone.com/LoIgCa. Which might be interesting for your `xxx.HolgerUtil` @Eugene ;) – Lino Aug 15 '18 at 12:27
  • @Lino indeed... I had already ideas to code such a thing, and you both bring great inspiration into the picture, thank you! – Eugene Aug 15 '18 at 12:40
  • @Eugene You're welcome, as a note: the `BiOptional` from me just executes when *both* values are present, and will do nothing when it's `empty`. So it behaves differently than the one provided in this answer – Lino Aug 15 '18 at 12:42
  • @Eugene Well, I *meant* `BiOptional` to correspond to `Optional` and - only as one of its features - to solve the OP's problem. Now I see, however, that it'd have problematic consequences, namely it's frowned upon to use `Optional` in non-return context, and here I use `BiOptional` in such a way. So I agree about those extra methods. As to `areNonePresent` in general: note that `Optional` will finally get an [`isEmpty`](https://download.java.net/java/early_access/jdk11/docs/api/java.base/java/util/Optional.html#isEmpty()) method in Java 11. All in all, I'm glad you liked it, thank you :) – Tomasz Linkowski Aug 15 '18 at 16:23
  • 1
    @Lino Your `BiOptional` is interesting, although I'm not sure I have ever encountered a use case for it :) But in retrospect, both your and my `BiOptional` have the main drawback of being too general (like tuples), so after all, I think I agree more with your treating this class in this Q&A more as a helper class than a full `BiOptional`. It just doesn't encourage uses other than "handling" and "mapping", like using `BiOptional` as a return type, which brings us too close too tuples (which Java [rather won't have](http://mail.openjdk.java.net/pipermail/core-libs-dev/2010-March/003995.html)). – Tomasz Linkowski Aug 15 '18 at 16:30
  • @TomaszLinkowski you say finally will get `isEmpty` meaning you like that? Sorry, I really didn't understand that addition. Well, could be used as a method reference Predicate I guess `.filter(Optional::isEmpty)` but same thing could be done via `.filter(x -> !x.isPresent())` or in java-11 `Predicate.not(Optional::isPresent)`... any other reasons I might be missing that you know about? – Eugene Aug 16 '18 at 07:49
  • @Eugene Yes, I like `Optional.isEmpty` because I like clarity. I prefer `if (optional.isEmpty())` over `if (!optional.isPresent())`, and I *much* prefer `.anyMatch(Optional::isEmpty)` than `.anyMatch(x -> !x.isPresent())` or even `.anyMatch(Predicate.not(Optional::isPresent))`. Other examples of such duality: `Objects.isNull/isNonNull`, `Stream.anyMatch/noneMatch`, JUnit's `Assert.assertFalse/True`, [`StringUtils.isEmpty/isNotEmpty`](https://commons.apache.org/proper/commons-lang/), jOOλ's [`Seq.isEmpty/isNotEmpty`](https://www.jooq.org/products/jOOλ/javadoc/0.9.14/org/jooq/lambda/Seq.html). – Tomasz Linkowski Aug 16 '18 at 08:19
  • 1
    @TomaszLinkowski in *such* a case let's propose `Collections`: `isEmpty` and `isNotEmpty` and the same for every single `Map` and for `String` and for every single utility does around `String`.. I bet I can find lot's of other cases too. This is not something we will agree on, but we don't have to, each person has it's take. We prohibit this in our code, well *our* is the keyword here. thx for your time, should I start looking into adding `jOOλ` to our project? hmmm – Eugene Aug 16 '18 at 08:22
  • @Eugene Let's continue in [chat](https://chat.stackoverflow.com/rooms/178118/room-for-tomasz-linkowski-and-eugene). – Tomasz Linkowski Aug 16 '18 at 09:15
3

To avoid if statements or here if (Optional.isPresent()) you should have a common way to handle the Optional values but it is not the case as according their content you may invoke a function with the functional interface Consumer<String> or BiConsumer<String, String>.

As hint, you may factor the second part but it is not more readable or a better way :

if (o1.isPresent() && o2.isPresent()) {
    handler1(o1.get(), o2.get());
} else {
    Map<Optional<String>, Consumer<String>> map = new HashMap<>();
    map.put(o1, this::handler2);
    map.put(o2, this::handler3);
    Optional<String> opt = Stream.of(o1, o2)
                                 .filter(Optional::isPresent)
                                 .findFirst()
                                 .orElseThrow(Exception::new);

    map.get(opt)
       .accept(opt.get());
}

If you have much more Optionals to handle in this way such as this would probably make more sense but still it is a lot of code to write.


A more readable alternative could be to introduce a Rule class that stores the information required to trigger that if required :

public Rule(BiPredicate<Optional<String>, Optional<String>> ruleFunction, Runnable runnableIfApplied) {
    this.ruleFunction = ruleFunction;
    this.runnable = runnableIfApplied;
}

The BiPredicate<Optional<String>, Optional<String>> represents the matching function and the Runnable is the method to execute if the matching occurs.
You could move the rule execution logic in a Rule static method.
The idea is to make as clear as possible the rule specifications from the client side such as :

void example(Optional<String> o1, Optional<String> o2, Optional<String> o3) throws Exception {

    Rule.executeFirstMatchOrFail(o1, o2, 
                                   new Rule((opt1, opt2) -> opt1.isPresent() && opt2.isPresent(), () -> handler1(o1.get(), o2.get())),
                                   new Rule((opt1, opt2) -> opt1.isPresent(), () -> handler2(o1.get())), 
                                   new Rule((opt1, opt2) -> opt2.isPresent(), () -> handler3(o2.get())));
}

Rule could look like :

public class Rule {

    static void executeFirstMatchOrFail(Optional<String> o1, Optional<String> o2, Rule... rules) throws Exception {
        for (Rule rule : rules) {
            if (rule.apply(o1, o2)) {
                return;
            }
        }
        throw new Exception();
    }

    private Runnable runnable;
    private BiPredicate<Optional<String>, Optional<String>> ruleFunction;

    public Rule(BiPredicate<Optional<String>, Optional<String>> ruleFunction, Runnable runnableIfApplied) {
        this.ruleFunction = ruleFunction;
        this.runnable = runnableIfApplied;
    }

    public boolean apply(Optional<String> o1, Optional<String> o2) {
        if (ruleFunction.test(o1,o2)) {
            runnable.run();
            return true;
        }
        return false;
    }

}
davidxxx
  • 125,838
  • 23
  • 214
  • 215
  • Thanks for this answer. This does help but as you said it is much more applicable in cases where you have multiple Optionals. I guess I will have to stick with simple `if-else` in this case to maintain readability of code (unless there is some other advantage of using your approach?). – Nullpointer Aug 14 '18 at 19:33
  • @davidxxx well, it took me 5 minutes to understand the code vs 10 seconds for the `if/else`... that is above abusing I guess – Eugene Aug 14 '18 at 19:35
  • @Eugene With the actual code of the OP it complicates more things . I agree totally. – davidxxx Aug 14 '18 at 19:42
  • @Nullpointer You are welcome. There is really not ad advantage to use the code I write. This just a code that shows how avoid the conditional statements but it is more complex. So it also shows that for your case it is not relevant and the `if`s appear better. – davidxxx Aug 14 '18 at 19:44
  • Good addition, I like the concept of that rule. I would suggest to replace `BiFunction, Optional, Boolean>` with `BiPredicate, Optional` though – Lino Aug 15 '18 at 12:52
  • Hi Lino. Arg... I don't use often it. Thanks for this nice remark. I updated. – davidxxx Aug 15 '18 at 14:44
3

It doesn’t really answer your question, but since Java 9 I would prefer something along these lines:

    o1.ifPresentOrElse(s1 -> {
        o2.ifPresentOrElse(s2 -> {
               handler1(s1, s2);
        }, () -> {
               handler2(s1);
        });
    }, () -> {
        o2.ifPresentOrElse(s2 -> {
               handler3(s2);
        }, () -> {
               throw new IllegalArgumentException("Neither was present");
        });
    });

There’s a rule of thumb about Optional saying not to use isPresent and get. I do use them very occasionally; most often they are better avoided.

Ole V.V.
  • 81,772
  • 15
  • 137
  • 161
  • 6
    Thanks for suggesting this approach, but I think using simple `if-else` conditions is much readable as compared to this? – Nullpointer Aug 14 '18 at 19:27
  • 3
    note that throwing a checked `Exception` will not work – Lino Aug 14 '18 at 19:28
  • 3
    @OleV.V. as a side note there was a proposal to deprecate `get` entirely, thus java-10 has the addition of `orElseThrow()`... – Eugene Aug 14 '18 at 19:32
  • 1
    Correct, @Lino, that would have to be an unchecked exception, or the last `ifPresentOrElse` should be replaced by an application of `orElseThrow`. – Ole V.V. Aug 14 '18 at 19:38