For many years, Java language lacked reasonable support for functional programming. When lambdas appeared almost 8 years ago, it was a huge shift for everyone which paved the way to many of today’s main libraries and tools. However, soon after that I noticed a strange effect. Some developers were so impressed with new Java API-s built for lambdas (streams, optionals) that they started using them merely as a fancy replacement for trivial statements, such as IF. In this article I would like to discuss this problem deeper and show how it harms your application.

tl;dr;

Java Optional has a well-defined use case. Using it as a replacement for IF harms readability and causes issues while debugging. Bad habits can even cause errors when switching to writing in Kotlin.

Example code

Some time ago I got a pull request to review. While looking at the code, I noticed a method similar to the one below:

public void process(Data data) {
    Optional.ofNullable(this.configuration)
       .map(it -> doSomething(data, it))
       .orElse(() -> doSomethingElse(data))
}

This code performs one of two alternative operations on the passed Data instance, depending on presence of some configuration. Figuring it out should be relatively easy. Indeed, I did not have any problem with that – I had to stop to understand what this code was supposed to do. I knew what Optional means, so seeing it here, out-of-context in a method that doesn’t return anything was a surprise. I started wondering if I missed something important, but no. Finally, I wrote a review comment asking to replace it with a good, old IF statement for the sake of readability.

Use case for Java Optional

Let’s remind why we have Optional in Java:

Optional is intended to provide a limited mechanism for library method return types where there needed to be a clear way to represent “no result,” and using null for such was overwhelmingly likely to cause errors.

Brian Goetz, Java language architect

The sole purpose of Optional is forcing third-party users of your API to handle returned empty values. Why only return values, but not method arguments? It’s all about control. We control our own methods: if someone passes null as an argument, we can detect it and react. On the other hand, we cannot do it with return values. If we need to return null, we can only hope that the caller remembers to handle it.

Optional is a wrapper around the original value that guards the access to it, ensuring that we handled empty results somehow. However, it comes at the cost of verbose syntax and additional overhead. Therefore, we should not use it internally within our own code. The reason is the same: you control this code, and there are better ways to protect it from null values (for example, static code analysis and annotations such as @Nullable).

In short…

If you control both the method and the place where it is called, you should not use Optional.

Case for IF statements

There is a lot of code which is complex, because the solved problem is complex. With divide-and-conquer approach, we can hide some of this complexity behind a domain-specific language (DSL). Java Optional is an example of such a DSL designed for a specific purpose, similarly as streams. But this complexity is still there, and sometimes the DSL is not trivial on its own, too. So why using them correctly matters so much?

The first reason is readability. Some people may shout here: wait a sec, but Optionals and streams do improve readability! Yes, they improve readability, but – like every DSL – only if we use them both correctly and in the right situation. DSL used incorrectly (e.g. using .reduce() for making network calls) gives us a cryptic code. You need a deep knowledge of internals and some free time to understand what the author meant. It defeats the purpose of using DSL and may be harder to read for inexperienced developers.

Another motivation is debugging. Even the most advanced IDE-s have a hard time while debugging a complex DSL. Even with such a simple call as .forEach(it -> something) it is easy to end up in the middle of some strange low-level code. This is why you should leave complex tools for complex stuff and keep simple things simple. Below, I show the only valid implementation of our sample – it uses a good, old IF statement. Almost every programmer on this planet will understand it, and so will the debugger:

public void process(Data data) {
    if (null == this.configuration) {
        doSomething(data, it)
    } else {
        doSomethingElse(data)
    }
}

In short…

Keep simple things simple, and leave complex DSL-s for complex problems. Also, simple statements like for and if are more debugger-friendly.

IF statements vs Kotlin

I could verify some of the observations presented above when I started programming in Kotlin. Contrary to Java, null-safety is built into the type system of Kotlin, and we must always explicitly say whether we permit nulls or not. It means that in this language, we don’t need Java Optional at all, but things are not that easy. In Kotlin, there is a strong push for creating DSL-s, and the language has a lot of extra idiomatic constructs. It turns out that they suffer from almost identical effect of trying “to be fancy”. One day I wrote the following code:

fun convert(input: Bar?): Foo = if (input != null) {
       toFoo(input) // this line was a bit larger in reality than in this example
    } else {
       Foo.empty()
    }

To my surprise, another developer asked me at the code review to change it to “more idiomatic Kotlin”:

fun convert(input: Bar?) = input?.let { toFoo(it) } ?: Foo.empty()

If you are not familiar with Kotlin, you can now easily compare the two versions and see which one is more newcomer-friendly. Even after a couple of months of writing in Kotlin, it can be a bit cryptic. And indeed, it is so cryptic that many Kotlin programmers fail to answer correctly what it does :). It is not a drop-in replacement for IF statement. In certain circumstances, both “branches” of our alternative may get executed. If we look at Kotlin documentation about scope functions (such as .let()), we can find:

Although the scope functions are a way of making the code more concise, avoid overusing them: it can decrease your code readability and lead to errors. Avoid nesting scope functions and be careful when chaining them: it’s easy to get confused about the current context object and the value of this or it.

Kotlin documentation

The lesson: a composition of several idiomatic elements doesn’t have to be idiomatic. In fact, it may be an antipattern. This is another reason for keeping things simple. Eventually, I also defended my version with good, old IF.

Conclusion

There’s nothing wrong with the good, old IF statement. Do not throw away “old” tools just because something new arrived. Learn what problem the new tool solves and use it properly.

Subscribe
Notify of
guest

2 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
BB ITF
BB ITF
11 months ago

The GitHub link is dead.