Recently I came across a piece of code which looked something like this:
final SomeStatus result = Optional.ofNullable(dataObject.someField)
.map(someField -> STATUS_IF_SOME_FIELD_EXISTS)
.orElse(STATUS_IF_SOME_FIELD_IS_MISSING);
Meaning that for dataObject.someField
not being null, it maps to STATUS_IF_SOME_FIELD_EXISTS
. Otherwise, it defaults to STATUS_IF_SOME_FIELD_IS_MISSING
. For example, if an User
has an email
, the status of that user is CONTACTABLE
otherwise UNCONTACTABLE
.
The use of Optional
in a control-flow context is peculiar, especially because Optional
is typically used in the context of an argument- or return-type. Without using an Optional
, the above example could be written as a ternary statement:
final SomeStatus result = dataObject.someField == null
? STATUS_IF_SOME_FIELD_IS_MISSING
: STATUS_IF_SOME_FIELD_EXISTS;
Depending on your preference or style guide, it can also be split up in an if-else block:
final SomeStatus result;
if (dataObject.someField == null) {
result = STATUS_IF_SOME_FIELD_IS_MISSING;
} else {
result = STATUS_IF_SOME_FIELD_EXISTS;
}
What looks better?
If you put 10 developers in a room and ask them, two or three camps will form and they will probably spend the entire day having a heated discussion! Some will prefer the ternary one-liner, one group will like the functional syntax of Optional
and the rest will argue that if-else will be immediately understood by everyone.
Personally I like the syntactic sugar of Optional
but the ternary statement is much simpler, one line and will probably be immediately understood by anyone with basic knowledge of Java (and C-style) syntax. The if-else statement is unnecessarily verbose for just variable assignment. My gut feeling also indicates that the Optional
would perform worse.
What performs better?
A benchmark can be easily done with the Java Microbenchmark Harness of OpenJDK. Only the Optional
and ternary variant will be compared because the difference between if-else and a ternary statement will be negligible (as it's a shortcut) and probably result in exactly the same bytecode.
For brevity the SomeStatus
enum is omitted and a char
is used to flip between null and non-null:
private static final char STATUS_IF_SOME_FIELD_EXISTS = 'A';
private static final char STATUS_IF_SOME_FIELD_IS_MISSING = 'B';
A simple data-object is used for holding the nullable-field:
private record DataObject(String someField) {}
To keep the DataObject
instantiation out of the benchmark, a thread-scoped state with a non-null and null instance will suffice:
@State(Scope.Thread)
public static class DataObjectState {
final DataObject dataObjectWithNullField = new DataObject(null);
final DataObject dataObjectWithNonNullField = new DataObject("abc");
}
This state can then be injected into a benchmark. A consuming blackhole is also injected into the benchmark; this ensures that the JVM doesn't optimize anything away because the resulting variable is always used:
@Benchmark
public void withOptionalNull(final Blackhole blackhole, final DataObjectState state) {
final Character result = Optional.ofNullable(state.dataObjectWithNullField.someField)
.map(someField -> STATUS_IF_SOME_FIELD_EXISTS)
.orElse(STATUS_IF_SOME_FIELD_IS_MISSING);
blackhole.consume(result);
}
Another benchmark is added for the non-null variety. In this case we use the DataObject
from the state which has a non-null field:
@Benchmark
public void withOptionalNonNull(final Blackhole blackhole, final DataObjectState state) {
final Character result = Optional.ofNullable(state.dataObjectWithNonNullField.someField)
.map(someField -> STATUS_IF_SOME_FIELD_EXISTS)
.orElse(STATUS_IF_SOME_FIELD_IS_MISSING);
blackhole.consume(result);
}
And naturally there will also be benchmarks for the counterpart which do not use an Optional
but a ternary-operator:
@Benchmark
public void withTernaryNull(final Blackhole blackhole, final DataObjectState state) {
final Character result = state.dataObjectWithNullField.someField == null
? STATUS_IF_SOME_FIELD_IS_MISSING
: STATUS_IF_SOME_FIELD_EXISTS;
blackhole.consume(result);
}
@Benchmark
public void withTernaryNonNull(final Blackhole blackhole, final DataObjectState state) {
final Character result = state.dataObjectWithNonNullField.someField == null
? STATUS_IF_SOME_FIELD_IS_MISSING
: STATUS_IF_SOME_FIELD_EXISTS;
blackhole.consume(result);
}
The code in its entirety can be found here.
The results
Benchmark Mode Cnt Score Error Units
withOptionalNonNull thrpt 5 441306314.280 ± 2455716.883 ops/s
withOptionalNull thrpt 5 1181029436.859 ± 22036897.132 ops/s
withTernaryNonNull thrpt 5 1393363778.290 ± 13873658.503 ops/s
withTernaryNull thrpt 5 1394910788.268 ± 9892487.907 ops/s
(with JDK 17.0.5)
As seen above, the ternary varieties have basically identical performance and get the best score (most operations per second). There is a notable difference between Optional
on a null and non-null value. This is because internally Optional.ofNullable
with a null value, will short-circuit to an empty Optional
:
public static <T> Optional<T> ofNullable(T value) {
return value == null
? (Optional<T>) EMPTY
: new Optional<>(value);
}
Subsequently, if an Optional
is empty, the map
method will barely do anything and immediately return an empty Optional
. In case of a non-null value, the Supplier
lambda in the map
will be actually executed and the result will again be wrapped in a nullable Optional
. This results in the performance penalty as several more call-stacks occur.
In case of a non-null value, the Optional
variant performs roughly 300% worse compared to the ternary statement. If Optional
starts with a null value, the performance difference is roughly 20%. In terms of actual time per operation, similar differences come to light:
Benchmark Mode Cnt Score Error Units
withOptionalNonNull avgt 5 2.344 ± 0.032 ns/op
withOptionalNull avgt 5 1.211 ± 0.016 ns/op
withTernaryNonNull avgt 5 0.724 ± 0.012 ns/op
withTernaryNull avgt 5 0.719 ± 0.007 ns/op
(with JDK 17.0.5)
Note that this benchmark is very bare-bones and is only measuring a very specific scenario. Feel free to experiment what happens when - for example - multiple map
calls are chained.
Is it significant enough?
Something performing 300% worse is significant in a relative sense but in absolute numbers, we are talking about nanoseconds here (1/1 000 000 of a millisecond). Meaning that it takes a lot of those Optional
control-flows to measure any significant difference. The chance Optional
is the root of your performance problems is unlikely. Always use profiling to find out where your actual performance bottlenecks are. Don't waste time on premature optimization.
Out of curiosity I performed a benchmark, but the performance penalty was not the deciding factor to change the example given in the beginning: though novel and functionally expressive, the ternary variant requires less cognitive work to understand and is in this case a simple one-liner. It's a small and safe change as unit tests are covering both control-flow branches. If, for example, the Optional
had more chained map
s, filter
s and additional steps, the choice might be different.
Wrapping a variable in an Optional
for a single null-check? I personally wouldn't. However, I would still use Optional
without hesitation for return- and argument-types: it adds expressiveness and can remove a lot of redundant null-checking.
Want more content like this?
Hi! I sporadically write about tech & business. If you want an email when something new comes out, feel free to subscribe. It's free and I hate spam as much as you do.