fbpx
Gå till innehållsmeny
Tjänst, projekt, namn...

Kontakta oss

Välj stad

An optional epiphany

Java’s Optional class is great — it gives your methods a way to signal to their callers that they might not have a proper answer to give. And that without returning null or throwing an exception. Using the Optional methods map(), orElse(), orElseGet() and orElseThrow() can be a great way to skip null checks and to make your code more fluent. However, there’s a nasty catch here, hiding in the bushes, which might bite you if you’re not careful enough. Read on to learn all about it if you dare.

It all started with a simple test. We’ve pre-populated a database with some data and this particular test was written to validate that the data (in this case, a report period) was fetched properly (method findByPeriodId() returns an Optional):

@Test
void shouldFetchPeriod() {
final var period = periodService.findByPeriodId(44)
.orElse(fail("Could not find period 44"));

assertThat(period.getPeriodId(), is(44));
// some more asserts
}

Simple, right? But for some reason, it failed. The error I got was this:

AssertionFailedError: Could not find period 44

This baffled me, because I could see that the pre-loaded data was actually there in other tests and that our frameworks (we use Testcontainers with a Mysql container and use Flyway to populate it) worked as they should.

So what could possibly go wrong? Here was my usual suspects list:

  1. The particular period (44) was not populated properly
  2. Period 44 was there, but someone wrote a test that deleted it
  3. PeriodService was not working as it should
  4. PeriodRepository (called by PeriodService) was not working as it should
  5. Intellij was playing a prank on me

Debugging the code revealed that all of these suspects had reliable alibis (including Intellij, after invalidating it’s cache and restarting it). So I decided that the culprit must be the Optional and focused my attention on it.

The following is an excerpt from the interrogation that followed:
me: why are you failing? I can see that you’re holding period 44. What are you playing at?
op: …
me: playing tough huh? what are you hiding? 
orElse() should only be called if the optional value is null, which I can clearly see it is not.
op: …
me: show me your 
orElse() implementation!
op: ok…

/**
* If a value is present, returns the value, otherwise returns
* {
@code other}.
*
*
@param other the value to be returned, if no value is present.
* May be {
@code null}.
*
@return the value, if present, otherwise {@code other}
*/
public T orElse(T other) {
return value != null ? value : other;
}

me: ok, so value, which in this case is the instance of Period , is not null, so the other value should not be returned. Hmmm…

And then it stroke me. What is other, anyway? It’s an argument that is sent to a method. An evaluated argument. If you have a method that looks like this:

void printme(int x) {
System.out.println(x + " is an fine looking int");
}

…and you call it like this:

printme(2 + 3);

…then 2 + 3 will be evaluated before they are sent to the method. printme() will be called with argument 5. And this is exactly what happened in my test! The argument was evaluated before orElse() was called! and in my case it was… fail().

With that in mind, I understood two things:

  1. I’m stupid
  2. There should be a better way

Reiterating through these two facts, I realised that the solution was right there, in front of me. And it was called orElseGet(). It’s implementation looks like this:

/**
* If a value is present, returns the value, otherwise returns the result
* produced by the supplying function.
*
*
@param supplier the supplying function that produces a value to be returned
*
@return the value, if present, otherwise the result produced by the
* supplying function
*
@throws NullPointerException if no value is present and the supplying
* function is {
@code null}
*/
public T orElseGet(Supplier<? extends T> supplier) {
return value != null ? value : supplier.get();
}

Unlike orElse() , which takes an evaluated value as argument, orElseGet receives a Supplier, which only gets invoked when needed, which in my case should be never. As shown above, the value is not null, hence the supplier will not get invoked and fail() will not be called. The new test now looked like this:

@Test
void shouldFetchPeriod() {
final var period = periodService.findByPeriodId(44)
.orElseGet(() -> fail("Could not find period 44"));

assertThat(period.getPeriodId(), is(44));
// some more asserts
}

Amazingly, or perhaps not so amazingly, it didn’t fail anymore.

Perhaps you saw my mistake immediately and thought that this whole article was silly as it was obvious to you that fail() was called directly when the argument was evaluated. But for me this was no less than an epiphany. And I finally understood why both of these methods, orElse() and orElseGet() exist. I will never doubt an Optional again.

By the way, what happens if the Optional that is returned from a method is neither empty nor containing a value, but is simply null? Eh…. let’s just pretend I never said that.

  • Written by Uzi Landsmann

Photo by Simon Berger on Unsplash

Relaterat / Kontaktpersoner

Vedlagte filer