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 maps, filters 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.

your@email.com Subscribe

In the wild: Java's Optional for control-flow