πŸ• Reading time: 4 minutes

When working with Optional and Mono in Java, it's common to have to handle fallback values. In this pill we explore the orElse, orElseGet, switchIfEmpty and defer methods, comparing the eager and lazy strategies. The idea comes from an unwanted side-effect that happened on the project I'm currently assigned to, caused by an improper use of the eager strategy with switchIfEmpty.

πŸ“Œ Differences between orElse and orElseGet

Both orElse and orElseGet from the Optional class let you define an alternative value, but they take different approaches. Let's imagine a function that returns the string "fallback" and increments an AtomicInteger every time it's called. In the test_orElse() and test_orElseGet() methods (see the code below), both Optionals contain the value "sample", so the fallback value isn't used. However, orElse runs function.apply(atomic) anyway, incrementing atomic: this approach is called eager, "early execution". orElseGet, on the other hand, leaves atomic unchanged, since it only runs the function if the value is absent, adopting a lazy strategy, "deferred execution".

πŸ“Œ Reactive context with switchIfEmpty and defer

We can replicate these behaviours in reactive contexts too. Let's look at the test_switchIfEmpty() and test_switchIfEmpty_usingDeferMethod() methods. In the first case, the function runs straight away, even though Mono.just("sample") already has a value. In the second case, instead, execution is deferred via the defer method, adopting the lazy strategy.

πŸ“‘ The test that compares the four cases

private static final String FOO = "notEmpty";

private static final Function<AtomicInteger, String> function = atomicValue -> {
    atomicValue.getAndIncrement();
    return "fallback";
};

private static AtomicInteger atomic;

@BeforeEach
void init_newAtomicInteger_with0AsInitialValue() {
    atomic = new AtomicInteger();
}

@Test
void test_orElse() {
    assertEquals(FOO, Optional.of(FOO).orElse(function.apply(atomic)));
    assertEquals(1, atomic.get()); // eager -> increment occurred
}

@Test
void test_orElseGet() {
    assertEquals(FOO, Optional.of(FOO).orElseGet(() -> function.apply(atomic)));
    assertEquals(0, atomic.get()); // lazy -> no increment
}

@Test
void test_switchIfEmpty() {
    assertEquals(FOO, Mono.just(FOO).switchIfEmpty(Mono.just(function.apply(atomic))).block());
    assertEquals(1, atomic.get()); // eager -> increment occurred
}

@Test
void test_switchIfEmpty_usingDeferMethod() {
    assertEquals(FOO, Mono.just(FOO).switchIfEmpty(Mono.defer(() -> Mono.just(function.apply(atomic)))).block());
    assertEquals(0, atomic.get()); // lazy -> no increment
}

πŸ” When to use one or the other

When using the constructs just described, it's worth weighing up the differences between the eager and lazy strategies carefully:

Picking the right construct not only boosts the efficiency and maintainability of the code, it also preserves the integrity of the application's state and optimises overall performance.

See you at the next pill! β˜•