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 emptyLa 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 isPresentLi 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 emptyopt.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 exceptionorElse(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()— lanciaNoSuchElementExceptionse assente. La forma senza argomenti introdotta in Java 10 è l'equivalente moderno diopt.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 orElse — or 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 camponullè più breve e chiaro per "questa entità non ha un secondo nome." La soluzione corretta è un campo non-Optional che può esserenull, con un getter che restituisceOptional. - Un parametro di metodo. Non accettare
Optional<String>come argomento. Sovraccarica il metodo, o accettaStringe documenta chenullsignifica 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. UsaList<T>e filtra i null al confine, o usaflatMap(Optional::stream)per eliminare gli assenti in una pipeline. - Un modo per evitare tutti i
null. Java ha ancoranullin 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.
Cosa ricavare dall'esecuzione:
- I tre costruttori
of,empty,ofNullablecorrispondono 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. orElseha valutato il suo argomento ogni volta, anche quando l'optional era presente. Il supplier diorElseGetè stato eseguito solo quando necessario. UsaorElseper letterali economici eorElseGetper qualsiasi cosa allochi, interroghi o lanci.mapeflatMaphanno fatto sì che l'intera catenauserById(...).flatMap(User::address).map(Address::city)si leggesse come una singola pipeline — nessun controllonull, nessun if annidato, e qualsiasi passo vuoto si riduce aOptional.empty()alla fine.flatMap(Optional::stream)ha trasformato unoStream<Optional<User>>in unoStream<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 comeIntStream.findFirstrestituiscono. 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.addressera un campoOptional<Address>— va bene perché l'esempio voleva dimostrare l'API, ma nel codice di produzione il campo sarebbe unAddresspossibilmente-nullcon un getterOptional<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.