Java Optional

Difficilmente scrivo articoli su aspetti minimali del linguaggio Java, ma a mio avviso è importante comprendere a pieno il paradigma Optional in quanto, utilizzati in congiunzione con gli Stream e le lambda expression, consentono di generare codice snello, comprensibile ed esente da errori.

Definizione

Il tipo Optional<T> è stato introdotto in java 8 allo scopo di rappresentare oggetti che possono assumere valori nulli. Esistono specializzazioni della classe utilizzate per rappresentare alcuni tipi primitivi, quali: OptionalDouble, OptionalInt e OptionalLong.

In generale un oggetto Optional può trovarsi i due stati possibili:

  1. PRESENT: ovvero contiene un riferimento non nullo ad un oggetto di tipo T;
  2. ABSENT or EMPTY: in caso opposto.

Si presti attenzione al fatto che un oggetto Optional empty non è equivalente ad un oggetto nullo.

Per instanziare un nuovo oggetto di tipo Optional esistono diverse possibilità:

  1. Optional.of(): metodo statico che crea un Optional per un oggetto non nullo, altrimenti solleva l’eccezione NullPointerException.
  2. Optional.ofNullable(): metodo statico che crea un Optional per un oggetto che può essere nullo, nel caso restituendo un oggetto Optional.empty().
  3. Optional.empty(): metodo statico che restituisce un oggetto Optional empty.

Utilizzo

Esiste in rete un acceso dibattito sulla reale utilità di tale costrutto del linguaggio. Noi ci rifacciamo a quanto enunciato da Brian Goetz, Java Language Architect in Oracle:

Optional è destinato a fornire un semplice meccanismo per i tipi di ritorno restituiti dai metodo di libreria in cui esiste una chiara necessità di rappresentare valori nulli e dove l’utilizzo di tali valori è in grado di causare errori

Per comprendere le motivazioni consideriamo il caso in cui abbiamo un’API che consente di ricercare il nome di un cliente all’interno di una lista di oggetti di tipo Customer in base all’id. Il codice potrebbe essere del tipo:

Questo è un tipico caso in cui l’oggetto restituito potrebbe essere nullo e se il programmatore non ne è consapevole, o più semplicemente dimentica tale possibilità, potrebbero generarsi eccezioni non gestite di tipo NullPointerException.

Prima Soluzione

Lo stesso metodo utilizzando gli Strema (già descritti nell’articolo Collection e Stream in Java) si riduce ad un più sintetico:

Si noti che il metodo findFirst() restituisce un tipo Optional sul quale invochiamo poi il get() per ottenere l’oggetto Customer in esso referenziato. Sfortunatamente il metodo get() invocato su un Optional empty restituisce l’eccezione NoSuchElementException, quindi di fatto non abbiamo risolto il nostro problema. Ci viene in aiuto il metodo isPresent() che restituisce true se il riferimento non è nullo per cui lo statement di return diviene:

Sebbene il codice sia ora perfettamente funzionante, risulta sicuramente poco elegante e certamente non in linea con la semplicità con cui è stato ricercato il valore richiesto utilizzando gli Stream. Fortunatamente la classe Optional presenta metodi che consentono di evitare l’utilizzo dei metodi get() e isPresent() e di produrre codice più “elegante”. Tali metodi sono tutti quelli della famiglia orElse() che consentono di restituire un valore di default o una eccezione specifica se l’oggetto Optional si riferisce ad un valore nullo. Ad esempio il metodo completo potrebbe essere:

in cui nel caso di oggetto non trovato viene instanziato un oggetto Customer (di default) invocando il relativo costruttore, sul quale eseguire poi il metodo getName().

Seconda Soluzione

Il codice prodotto presenta ancora delle lacune. Innanzitutto richiede la creazione di un nuovo oggetto Customer, cosa che potrebbe in generale essere onerosa, ma cosa più importante restituisce la proprietà name dell’oggetto creato che potenzialmente potrebbe ancora essere nullo.

Ci viene in aiuto un altro metodo della classe Option, il metodo map() che, nel caso in cui  il valore sia presente, consente di applicare una funzione di mapping all’oggetto (si veda l’articolo Collection e Stream in Java per un ulteriore dettaglio sul mapping di Stream). La funzione di mapping è quella che mappa l’oggetto Customer nella proprietà nome dello stesso. Il codice risultante è:

Si noti che map() restituisce ancora un oggetto di tipo Optional.

Altri metodi di Utilità

La classe Optional presenta altri metodi che trovano utile applicazione in diversi casi concreti.

ifPresent

Tale metodo consente di eseguire una determinata funzione sull’oggetto nel caso in cui non sia empty. Ad esempio un codice del tipo:

con l’uso degli Optional può essere riscritto in:

filter()

Questa API serve ad eseguire un test sull’oggetto referenziato utilizzando un predicato di input come condizione di test. Ad esempio, per testare il valore di un campo contenente un anno, possiamo utilizzare ile seguenti espressioni:

Un esempio più complesso che consente di determinare se un nostro cliente è un teenager è il seguente metodo: