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:
- PRESENT: ovvero contiene un riferimento non nullo ad un oggetto di tipo
T
; - 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à:
Optional.of()
: metodo statico che crea unOptional
per un oggetto non nullo, altrimenti solleva l’eccezioneNullPointerException
.Optional.ofNullable()
: metodo statico che crea unOptional
per un oggetto che può essere nullo, nel caso restituendo un oggettoOptional.empty()
.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:
1 2 3 4 5 6 7 8 9 10 11 12 |
private List<Customer> customers = new ArrayList<Customer>(); public String searchNameById( String id ) { Customer result = null; for ( Customer customer : customers ) { if ( customer.getId().equalsIgnoreCase( id ) ) { result = customer; break; } } return result != null ? result.getName() : null; } |
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:
1 2 3 4 5 6 7 |
public String searchNameById( String id ) { Optional<Customer> result = customers.stream() .filter( c -> c.getId().equalsIgnoreCase( id ) ) .findFirst(); return result.get().getName(); } |
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:
1 |
return result.isPresent() ? result.get().getName() : "UNKNOWN"; |
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:
1 2 3 4 5 |
return customers.stream() .filter( c -> c.getId().equalsIgnoreCase( id ) ) .findFirst() .orElseGet( Customer::new ) .getName(); |
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 è:
1 2 3 4 5 |
return customers.stream() .filter( c -> c.getId().equalsIgnoreCase( id ) ) .findFirst() .map( c -> c.getName() ) .orElse( "UNKNOWN" ); |
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:
1 2 3 4 |
String str = ... if ( str != null ) { System.out.println( str.length() ); } |
Optional
può essere riscritto in:
1 2 |
Optional<String> str = ...; str.ifPresent( s -> System.out.println( s.length() ) ); |
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:
1 2 |
Optional<Integer> year = ...; year.filter(y -> y == 2016).isPresent(); |
Un esempio più complesso che consente di determinare se un nostro cliente è un teenager è il seguente metodo:
1 2 3 4 5 6 7 |
public boolean isTeenager(Customer customer) { return Optional.ofNullable( customer ) .map( Customer::getAge ) .filter( a -> a >= 13 ) .filter( a -> a <=19 ) .isPresent(); } |