Logo

Developer Blog

Anton Lijcklama à Nijeholt

Dedicated to cultivating engaging and supportive engineering cultures.

Be Aware of the Non-Lazy Nature of Java

Introduction #

When you get into the habit of functional programming, you probably start to love the composability and the readability that it brings to your codebase. You might start to see code more as a data transformation pipeline or “pieces of a puzzle” that you try to connect (i.e. functional composition). When chaining methods in Java, you can sometimes run into unexpected behaviour.

OrElse… #

At the client I’m working with, we make use of VAVR, a library that can help you out with many daily functional programming tasks. One of those is the Option type class, which is a better version than the default Optional in Java (see why java.util.Optional is broken).

Today I ran into an issue where some parts of my mock were triggered that were not supposed to be triggered. It was a pretty easy flow:

  • Get order by criteria X
  • If the order cannot be found, try criteria Y
  • If the order still cannot be found, try criteria Z

Simple right?

java
@Test
public void test() {
    Option<String> value = Option.<String>none()
        .orElse(doSomething("step 1"))
        .orElse(doSomething("step 2"))
        .orElse(doSomething("step 3"));
    System.out.println(value);
}
public Option<String> doSomething(String message) {
    System.out.println(message);
    return Option.some(message);
}

What will be the output of the snippet above?

text
step 1
step 2
step 3
Some(step 1)

You can see that the result is eventually Some(step 1), but in the meantime, all the potential logic of all the other steps has been executed too! That’s not what I intended when I wrote the above code…

How can we fix this? By returning functions, that are lazy by default:

java
@Test
public void test() {
    Option<String> value = Option.<String>none()
        .orElse(doSomething("step 1"))
        .orElse(doSomething("step 2"))
        .orElse(doSomething("step 3"));
    System.out.println(value);
}
public Function0<Option<String>> doSomething(String message) {
    return () -> {
        System.out.println(message);
        return Option.some(message);
    };
};

The output that is generated:

text
step 1
Some(step 1)

Conclusion #

Lesson learned? Using mocks saved my day. It made me aware of things that were going on that were not intended. This would have been much harder to figure out on production. It’s yet another reason why languages like Scala are better suited for functional programming on the JVM. Just look at the Scala version of the code:

scala
def doSomething(message: String): Option[String] = {
  System.out.println(message)
  Some(message)
}

def main(args: Array[String]): Unit = {
  val value = None.orElse(doSomething("step 1"))
      .orElse(doSomething("step 2"))
      .orElse(doSomething("step 3"))
  print(value)
}

Same flow and no extra cognitive load on the developer. It’s a huge benefit if you write in a language that has certain fundamentals in place regarding laziness. It makes you able to focus on flow and business logic instead of the low-level implementation details. Unless you are in a highly technical domain, you would like to let your thoughts stay on the domain level (i.e. the what, rather than the how).

Why does it work in Scala straight away? Because of something called by-name parameters, which is a core feature of the language. Click the link for more information on how this works.

Hope this was useful! If you have any questions, please let me know!

Posted on Dec 9, 2019 (updated on Jun 2, 2024)