W3docs

Java Optional

Esprimi l'assenza possibile di un valore in Java con Optional ed evita NullPointerException per progettazione.

Optional<T> è un contenitore che contiene o un valore di tipo T o nulla — e ti comunica quale, a livello di tipo, così il compilatore può obbligarti a gestire il caso assente. È stato aggiunto in Java 8 insieme agli stream, e i due sono progettati per funzionare insieme: findFirst, findAny, min, max, reduce restituiscono tutti Optional<T> proprio perché la risposta potrebbe non esistere, e l'API offre metodi fluenti per continuare a calcolare senza mai scrivere if (x != null).

Optional non è un sostituto universale di null, e il JDK è esplicito su dove appartiene. Questo capitolo percorre l'API dall'inizio alla fine, poi i tre casi in cui Optional è la scelta sbagliata.

Costruire un Optional

Tre costruttori, ognuno con un significato preciso:

Optional<String> a = Optional.of("hello");           // present; null arg throws NPE
Optional<String> b = Optional.empty();                // absent
Optional<String> c = Optional.ofNullable(maybeNull);  // present if non-null, else empty

La distinzione è importante. Optional.of(x) è l'asserzione "questo valore è sicuramente qui" — se passi null lancia immediatamente NullPointerException, che è quello che vuoi (un bug emerso alla sorgente, non tre frame più in basso). Optional.ofNullable(x) è l'adattatore che usi per avvolgere un'API legacy che restituisce null per indicare "assente."

Quasi mai costruisci un Optional manualmente all'interno di una pipeline stream — i terminali come findFirst e Collectors.maxBy li producono per te.

Verificare se un valore è presente

Le due query:

Optional<String> opt = lookup(id);
boolean has = opt.isPresent();      // true if a value is held
boolean none = opt.isEmpty();        // Java 11+ -- the opposite of isPresent

Li vedrai nel codice di produzione, ma di solito sono un code smell: la maggior parte del codice che chiama isPresent poi get si leggerebbe meglio come uno dei metodi opera-su-di-esso illustrati sotto. I metodi di query servono per il codice al confine dove hai davvero bisogno di un boolean — una clausola di guardia, una decisione di routing, un ramo con avviso loggato.

Leggere il valore in modo sicuro

Il modo sbagliato:

String name = opt.get();   // throws NoSuchElementException if empty

opt.get() è la lettura non verificata. È il modo in cui trasformi un Optional di nuovo in un valore e un'eccezione a runtime, esattamente ciò che il tipo avrebbe dovuto prevenire. Usalo solo dopo aver dimostrato che l'optional è presente (o dopo findFirst().orElseThrow() da una pipeline in cui il caso vuoto sarebbe un bug del programmatore, non un caso atteso).

I modi corretti, in ordine di preferenza:

String name1 = opt.orElse("anonymous");                          // default value
String name2 = opt.orElseGet(() -> expensiveDefault());          // lazy default
String name3 = opt.orElseThrow();                                 // NoSuchElementException
String name4 = opt.orElseThrow(() -> new MyDomainError(id));      // custom exception
  • orElse(value) — fornisce un valore di default. Il valore viene sempre valutato, anche quando l'optional è presente, quindi non passare un'espressione costosa.
  • orElseGet(supplier) — fornisce un valore di default in modo lazy. Il supplier viene eseguito solo quando l'optional è vuoto. Usalo per qualsiasi default che costi più di un letterale.
  • orElseThrow() — lancia NoSuchElementException se assente. La forma senza argomenti introdotta in Java 10 è l'equivalente moderno di opt.get() quando "questo deve assolutamente essere presente" è l'unica interpretazione sensata nel punto di chiamata.
  • orElseThrow(supplier) — lancia un'eccezione specifica del dominio. Il modo standard per tradurre "assente" in "404 not found."

Trasformare il valore — map

Se l'optional è presente, applica una funzione; altrimenti rimane vuoto:

Optional<String> upper = opt.map(String::toUpperCase);
Optional<Integer> len   = opt.map(String::length);

La firma è Optional<T>.map(Function<T, R>) -> Optional<R>. La funzione viene eseguita solo quando un valore è presente — nessun controllo null, nessun if, e nessun else. Questa è l'operazione che rende Optional utile: la maggior parte delle catene "se non-null, fai questo; se non-null, poi fai questo" si riducono in una .map(...).map(...).map(...).

C'è un caso speciale che il JDK gestisce silenziosamente: se la tua funzione map restituisce null (perché avvolge un'API legacy che restituisce null per "nessun risultato"), l'Optional risultante è empty() — non Optional.of(null).

Comporre optional — flatMap

Quando la funzione di mapping stessa restituisce un Optional, map produrrebbe Optional<Optional<T>>. flatMap lo appiattisce:

record User(String id, Optional<Address> address) {}
record Address(String city) {}

Optional<String> city = userById(id)
    .flatMap(User::address)        // Optional<Address>
    .map(Address::city);            // Optional<String>

flatMap è l'operazione che consente di concatenare diverse ricerche, ognuna delle quali può fallire, in una singola pipeline. Entrambi i casi di fallimento si riducono a Optional.empty() alla fine, e il consumatore li gestisce una volta sola con orElse / orElseThrow.

Filtrare — filter

Verifica il valore rispetto a un Predicate<T>; restituisce lo stesso optional se il predicato è soddisfatto, empty() altrimenti:

Optional<String> nonBlank = opt.filter(s -> !s.isBlank());
Optional<Integer> positive = numberOpt.filter(n -> n > 0);

Agisce come una guardia all'interno della pipeline optional. Utile quando la domanda è "ho un valore, ma è il valore giusto per continuare?"

Effetti collaterali — ifPresent, ifPresentOrElse

Esegui codice solo quando il valore è presente:

opt.ifPresent(name -> log.info("hello, {}", name));

Oppure esegui un ramo quando presente e un altro quando vuoto (Java 9+):

opt.ifPresentOrElse(
    name -> log.info("hello, {}", name),
    () -> log.warn("no name on the request"));

Questi sono il modo corretto per esprimere "fai qualcosa passando per qui." Sostituiscono completamente il pattern if (opt.isPresent()) { use(opt.get()); }.

Collegamento agli stream — Optional.stream()

(Java 9+) Trasforma un Optional<T> in uno Stream<T> di zero o un elemento:

Stream<String> s = opt.stream();

Utile all'interno di flatMap su uno Stream<Optional<T>>:

List<String> presentCities = userIds.stream()
    .map(this::userById)           // Stream<Optional<User>>
    .flatMap(Optional::stream)      // Stream<User>     -- empties drop, presents pass through
    .map(User::city)
    .toList();

Questo sostituisce filter(Optional::isPresent).map(Optional::get) con un singolo flatMap(Optional::stream). Stesso risultato, pipeline più pulita.

or — ricadere su un altro Optional

(Java 9+) Se vuoto, usa un supplier di un altro Optional:

Optional<User> u = primaryLookup(id)
    .or(() -> fallbackLookup(id))
    .or(() -> Optional.of(User.anonymous()));

Si legge come "prova il primario; se assente, prova il fallback; se assente, usa l'anonimo." Tutti e tre sono Optional<User>; la catena restituisce il primo non vuoto. Diverso da orElseor mantiene il risultato avvolto; orElse lo decomprime con un semplice default T.

Specializzazioni primitive

Esistono OptionalInt, OptionalLong, OptionalDouble per i risultati primitivi — quello che restituisce IntStream.max(), ad esempio:

OptionalInt max = nums.stream().mapToInt(Integer::intValue).max();
int hi = max.orElse(0);

Hanno un'API più piccola — niente map/flatMap/filter — perché si trovano al confine del mondo primitivo. Usali per leggere i risultati degli stream primitivi; converti a Optional<Integer> se hai bisogno dell'API completa.

Dove Optional non appartiene

L'intento di progettazione del JDK è preciso: Optional è un tipo di ritorno per i metodi la cui risposta potrebbe non esistere. Non è:

  • Un tipo di campo. Non scrivere private Optional<String> middleName;. Non è Serializable, costa un'allocazione per campo, e un campo null è più breve e chiaro per "questa entità non ha un secondo nome." La soluzione corretta è un campo non-Optional che può essere null, con un getter che restituisce Optional.
  • Un parametro di metodo. Non accettare Optional<String> come argomento. Sovraccarica il metodo, o accetta String e documenta che null significa assente. I parametri Optional obbligano il chiamante a wrappare, il che è rumore.
  • Un elemento di collezione. List<Optional<T>> è quasi sempre una lista con elementi nullable e wrapping extra. Usa List<T> e filtra i null al confine, o usa flatMap(Optional::stream) per eliminare gli assenti in una pipeline.
  • Un modo per evitare tutti i null. Java ha ancora null in ogni tipo riferimento; Optional è per la forma del ritorno del codice che produce valori che potrebbero non esistere. I tipi riferimento normali vanno bene per tutto il resto.

La regola più breve: un Optional che fluisce fuori da un metodo è un buon design; un Optional che fluisce dentro è quasi sempre sbagliato.

Un esempio completo: tutti i metodi, più le regole pratiche nel codice

Il programma qui sotto costruisce un piccolo grafo utente/indirizzo, percorre ogni metodo su Optional rispetto ad esso, dimostra il timing di valutazione di orElse vs. orElseGet, il bridge Optional.stream(), e la catena or.

java— editable, runs on the server

Cosa ricavare dall'esecuzione:

  • I tre costruttori of, empty, ofNullable corrispondono a tre intenti precisi: sicuramente presente, sicuramente assente, e adattatore-legacy, presente-se-non-null. Optional.of(null) lancia — ed è il fallimento desiderato, non un bug da aggirare.
  • orElse ha valutato il suo argomento ogni volta, anche quando l'optional era presente. Il supplier di orElseGet è stato eseguito solo quando necessario. Usa orElse per letterali economici e orElseGet per qualsiasi cosa allochi, interroghi o lanci.
  • map e flatMap hanno fatto sì che l'intera catena userById(...).flatMap(User::address).map(Address::city) si leggesse come una singola pipeline — nessun controllo null, nessun if annidato, e qualsiasi passo vuoto si riduce a Optional.empty() alla fine.
  • flatMap(Optional::stream) ha trasformato uno Stream<Optional<User>> in uno Stream<User> con tutti gli assenti eliminati in un colpo solo. Questo è il modo pulito per collegare una lista di ricerche "che possono fallire" in uno stream di successi.
  • OptionalInt è ciò che i terminali degli stream primitivi come IntStream.findFirst restituiscono. Ha la propria piccola API (getAsInt, orElse, ifPresent) ed esiste in modo che le pipeline primitive non debbano mai fare boxing.
  • La regola pratica sui "posti sbagliati" è emersa implicitamente: User.address era un campo Optional<Address> — va bene perché l'esempio voleva dimostrare l'API, ma nel codice di produzione il campo sarebbe un Address possibilmente-null con un getter Optional<Address> address() che esegue il wrapping.

Cosa c'è dopo

La parte 12 ha coperto il vocabolario funzionale dall'inizio alla fine: interfacce funzionali, lambda, riferimenti a metodi, i built-in, la pipeline stream, ogni sorgente, ogni intermedio, ogni terminale, i collector, l'esecuzione parallela, e infine Optional come espressione a livello di tipo dell'assenza. Il prossimo capitolo, Java Predicate Interface, torna a concentrarsi su una singola interfaccia funzionale — Predicate<T> — e l'algebra dei combinatori (and, or, negate, isEqual, not) che consente di assemblare predicati senza mai scrivere manualmente la logica booleana. Da lì la parte continua con Function, Consumer/Supplier, e la famiglia degli operatori binari — un'interfaccia per capitolo, ognuna con la stessa forma di esempio pratico vista qui.

Pratica

Pratica
Hai `Optional<String> opt` e hai bisogno di un default quando è vuoto, dove il default è una chiamata costosa a `loadDefaultFromDb()`. Quale è corretto *e* evita di eseguire la chiamata costosa quando `opt` è presente?
Hai `Optional<String> opt` e hai bisogno di un default quando è vuoto, dove il default è una chiamata costosa a `loadDefaultFromDb()`. Quale è corretto *e* evita di eseguire la chiamata costosa quando `opt` è presente?
Was this page helpful?