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?
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.
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.
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.
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.
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:
Optional<T> first()
and Optional<T> second()
methodsis(First/Second)Present
, is(First/Second)OnlyPresent
and are(Both/None)Present
methodsif(First/Second)Present
methods are renamed to if(First/Second)OnlyPresent
ifNonePresent(Runnable action)
methodorElseThrow
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:
on*Present
mapping methods, and then call R result()
(which will throw if result
were to be absent), orOptional<R> optionalResult()
Note also that:
BiOptional
: if*Present
BiOptionalMapper
: on*Present
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;
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With