π 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:
- choose
orElse(orswitchIfEmpty, in a reactive context) for simple values, but pay close attention: both methods trigger the fallback immediately, with the risk of running functions that alter variables or internal state even when their result won't be used. - go for
orElseGet(orswitchIfEmptycombined withdefer, in a reactive context) when fallbacks are "expensive" (such as calls to external systems) or when the fallback function modifies the state of existing objects. These methods only run the fallback when strictly necessary, avoiding wasted resources and preventing unwanted side-effects.
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! β