🕐 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:
- scegliere
orElse(oswitchIfEmpty, in contesto reattivo) per valori semplici, prestando però molta attenzione: entrambi i metodi attivano il fallback immediatamente, con il rischio di eseguire funzioni che alterano variabili o stati interni anche quando il loro risultato non verrà usato. - optare per
orElseGet(oswitchIfEmptyin combinazione condefer, in contesto reattivo) in caso di fallback "costosi" (come chiamate a sistemi esterni) o quando la funzione di fallback modifica lo stato di oggetti esistenti. Questi metodi eseguono il fallback solo se strettamente necessario, evitando sprechi di risorse e prevenendo effetti collaterali indesiderati.
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! ☕