🕐 Tempo di lettura: 4 minuti

Nell'uso di Optional e Mono in Java, è comune dover gestire valori di fallback. In questa pillola esploriamo i metodi orElse, orElseGet, switchIfEmpty e defer, mettendo a confronto le strategie eager e lazy. L'idea nasce da un effetto collaterale indesiderato verificatosi nel progetto cui sono attualmente allocato, a causa di un uso improprio della strategia eager con switchIfEmpty.

📌 Differenze tra orElse e orElseGet

Sia orElse che orElseGet della classe Optional permettono di definire un valore alternativo, ma adottano approcci differenti. Immaginiamo una funzione che restituisca la stringa "fallback" e incrementi un AtomicInteger ogni volta che viene chiamata. Nei metodi test_orElse() e test_orElseGet() (vedi codice sotto), entrambi gli Optional contengono il valore "sample", quindi il valore di fallback non viene utilizzato. Tuttavia, orElse esegue comunque la function.apply(atomic), incrementando atomic: questo approccio è chiamato eager, "esecuzione anticipata". orElseGet, invece, lascia atomic invariato, poiché esegue la funzione solo se il valore non è presente, adottando una strategia lazy, "esecuzione ritardata".

📌 Contesto reattivo con switchIfEmpty e defer

Anche nei contesti reattivi possiamo replicare questi comportamenti. Osserviamo i metodi test_switchIfEmpty() e test_switchIfEmpty_usingDeferMethod(). Nel primo caso, la function viene eseguita subito, nonostante Mono.just("sample") abbia già un valore. Nel secondo caso, invece, l'esecuzione viene ritardata tramite il metodo defer, adottando la strategia lazy.

📑 Il test che mette a confronto i quattro casi

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
}

🔍 Quando usare l'uno o l'altro

Quando si utilizzano i costrutti appena descritti, è opportuno valutare attentamente le differenze tra le strategie eager e lazy:

La scelta del costrutto corretto non solo aumenta l'efficienza e la manutenibilità del codice, ma preserva anche l'integrità dello stato dell'applicazione e ottimizza le prestazioni complessive.

Alla prossima pillola! ☕