Java 8: gli Optional

Java 8 introduce il nuovo tipo Optional, in questo post andremo a vedere quali sono le sue caratteristiche e dove viene impiegato negli stream. Come nei post precedenti su Java 8, vogliamo partire dal codice che usiamo oggi e vedere come questa nuova versione di Java lo può migliorare. In questo post vogliamo modellare una lista di oggetti appartamento e costruire una funzione che ne dia un punteggio in base alle caratteristiche. Iniziamo quindi dalla classe Apartment che ha il numero che identifica l’interno, il numero di stanze e l’oggetto Parking che rappresenta le caratteristiche del parcheggio di pertinenza.

public class Apartment {

   private int number;
   private int numberOfRooms;
   private Parking parking;

   public int getNumberOfRooms() {
      return numberOfRooms;
   }

   public void setNumberOfRooms(int numberOfRooms) {
      this.numberOfRooms = numberOfRooms;
   }

   public int getNumber() {
      return number;
   }

   public void setNumber(int number) {
      this.number = number;
   }

   public Parking getParking() {
      return parking;
   }

   public void setParking(Parking parking) {
      this.parking = parking;
   }

}

Un parcheggio definisce un numero di posti disponibili e il tipo: all’aperto oppure al coperto.

public enum ParkingType {
   OUTDOOR, COVERED
}

public class Parking {

   int numberOfPlaces;
   ParkingType parkingType;

   public int getNumberOfPlaces() {
      return numberOfPlaces;
   }

   public void setNumberOfPlaces(int numberOfPlaces) {
      this.numberOfPlaces = numberOfPlaces;
   }

   public ParkingType getParkingType() {
      return parkingType;
   }

   public void setParkingType(ParkingType parkingType) {
      this.parkingType = parkingType;
   }

}

Il nostro modello è completo, ora definiamo a parole la funzione di punteggio: un appartamento guadagna 20 punti per ogni stanza e 4 punti per ogni posto auto. Se il parcheggio è al coperto ci sono altri 5 punti aggiuntivi. Detto con il codice, invece, scriviamo:

public static int getScore(Apartment apt) {
   int score = 0;
   if (apt != null) {
      score = apt.getNumberOfRooms() * 20;
      score += apt.getParking().getNumberOfPlaces() * 4;

      if (apt.getParking().getParkingType() == ParkingType.COVERED)
         score += 5;
   }
   return score;

}

Non fidarti dei dati dell’appartamento 23

Siamo pronti a dare un punteggio dell’appartamento 23:

Apartment apt23 = new Apartment();
apt23.setNumber(23);
apt23.setNumberOfRooms(3);

System.out.println(getScore(apt23));

e il risultato è:

Exception in thread "main" java.lang.NullPointerException
	at it.cosenonjaviste.optional.Java8Optional.getScore(Java8Optional.java:57)
	at it.cosenonjaviste.optional.Java8Optional.main(Java8Optional.java:22)

Purtroppo quello che vediamo in console, non è il punteggio calcolato ma una NullPointerException: la nostra funzione non tiene in considerazione gli appartamenti che non hanno parcheggio. Giusto, basta cambiare un attimo le cose e risolviamo subito il problema.

public static int getScore(Apartment apt) {
   int score = 0;
   if (apt != null) {
      score = apt.getNumberOfRooms() * 20;
      if (apt.getParking() != null) {
         score += apt.getParking().getNumberOfPlaces() * 4;
         if (apt.getParking().getParkingType() != null) {
            if (apt.getParking().getParkingType() == ParkingType.COVERED)
               score += 5;
         }
      }
   }
   return score;
}

Cosa ci può venire in mente dopo aver visto l’ennesima NPE? Forse niente: semplicemente non ci eravamo accorti che il metodo potesse tornare null. Oppure possiamo pensare che la classe non fosse documentata a dovere e che nessuno ci avesse avvisato della possibilità che Parking fosse vuoto.

Però perché accanirsi su di una singola classe (e sul suo autore 😉 ), quando invece possiamo fare le cose “un po’ più in grande” e criticare direttamente l’esistenza dei null all’interno del linguaggio? In fin dei conti non tutti i linguaggi hanno il concetto di null reference/pointer e forse siamo talmente abituati ad avere i null intorno, che neppure immaginiamo che siano sbagliati. Beh, qualcuno questo pensiero l’ha fatto e ha tutto il titolo per farlo, si tratta del signor Hoare (inventore tra l’altro di Quicksort e Algol) e del suo errore da un miliardo di dollari:

I call it my billion-dollar mistake. It was the invention of the null reference in 1965. At that time, I was designing the first comprehensive type system for references in an object oriented language (ALGOL W). My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn’t resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.

Quindi sono circa cinquanta (!) anni che diversi linguaggi di programmazione si portano dietro questa caratteristica e Java è tra questi. Dalla versione 8 però è stato introdotto il nuovo tipo Optional per aiutarci a arginare questo tipo di problema.

Optional è una classe che può contenere o meno il reference di un oggetto di un altro tipo (ed è quindi un tipo parametrico). Al posto di avere un metodo che ritorna un reference potenzialmente nullo, possiamo restituire un Optional che a sua volta contiene o meno il reference all’oggetto voluto. Il concetto è semplice, ma la differenza è sostanziale: introducendo gli Optional costringiamo il codice client a gestire a livello di compilazione la situazione in cui potrebbe o non potrebbe ricevere qualcosa dal metodo chiamato. Questo chiaramente è molto più vincolante dell’informazione fornita da documentazione o commenti. Cambiamo subito le nostre classi.

public class Apartment {

   //as before

   public Optional<Parking> getParking() {
      return Optional.ofNullable(parking);
   }

   // as before

}

public class Parking {

   // as before

   public Optional<ParkingType> getParkingType() {
      return Optional.ofNullable(parkingType);
   }

  // as before

}

Possiamo ottenere un Optional tramite tre metodi di factory:

  • usando ofNullable per inscatolare un reference che può essere o meno vuoto come nel nostro esempio,
  • usando il metodo of che permette di costruire un Optional a partire da un reference non nullo e
  • tramite empty che rappresenta l’Optional vuoto.

Avendo per le mani un Optional, possiamo scoprire se contiene qualcosa tramite il metodo isPresent ed estrarne il contenuto con il metodo get. Il codice per calcolare il punteggio può essere quindi riscritto in maniera meccanica nel seguente modo.

public static int getScore(Apartment apt) {
    int score = 0;
    if (apt != null) {
       score = apt.getNumberOfRooms() * 20;
       if (apt.getParking().isPresent()) {
          score += apt.getParking().get().getNumberOfPlaces() * 4;
          if (apt.getParking().get().getParkingType().isPresent()) {
             if (apt.getParking().get().getParkingType().get() == ParkingType.COVERED)
                score += 5;
          }
       }
    }
    return score;
}

Chi ha detto Guava?

La classe Optional di Java è molto simile a Optional di Guava e in effetti nascono con lo stesso intento: costringere a gestire la presenza o l’assenza di un oggetto. A conti fatti, entrambe ottengono il risultato voluto, ma l’ultimo codice che abbiamo scritto è sicuramente poco agevole da leggere. Tra l’altro, se chiamiamo il metodo get prima di aver verificato che isPresent torni true, otteniamo una NoSuchElementException facendoci tornare al punto di partenza. L’Optional di Java nasce però già con il supporto alle lambda expression e quindi la versione precedente del metodo score può essere riscritta in forma più funzionale e di conseguenza in modo più compatto.

Optional ha a disposizione i metodi map, flatMap e filter che avevamo già visto con gli stream, in questo contesto però assumono un significato leggermente diverso che andiamo a vedere usando degli esempi presi o ispirati dalla funzione di punteggio.

Il metodo map

Il metodo map prende in ingresso una Function dal tipo T, che è quello dell’Optional di partenza, ad un tipo V. Se l’Optional contiene qualcosa viene applicata la trasformazione, se invece l’Optional è vuoto, viene restituito nuovamente un Optional vuoto. Quindi al posto di scrivere

if (apt.getParking() != null) {
    score += apt.getParking().getNumberOfPlaces() * 4;

possiamo scrivere

score += apt.getParking()
            .map(p -> p.getNumberOfPlaces() * 4)
			.orElse(0);

Come detto, map restituisce un Optional dove nel nostro caso V è Integer. Per estrarre il valore dall’Optional possiamo usare il metodo get, ma come sappiamo, l’Optional potrebbe essere vuoto. Per essere sicuri di restituire sempre qualcosa usiamo orElse che ritorna il valore dell’Optional se presente oppure il suo parametro altrimenti. Nel nostro caso, se manca il parcheggio viene restituito lo zero.

Il metodo flatMap

Consideriamo adesso questo codice sempre preso dalla prima versione del metodo score:

if (apt.getParking() != null && (apt.getParking().getParkingType() != null))
   // do something

Stiamo estraendo un oggetto e, se c’è, estraiamo un altro oggetto contenuto nel primo per farci qualcosa. Praticamente stiamo controllando i nostri reference in cascata. Se ci limitiamo ad usare il metodo map

apt.getParking().map(p -> p.getParkingType());

quello che otteniamo è un Optional<Optional<ParkingType>> che non è agevole da utilizzare. Ci piacerebbe rimuovere almeno un livello di “incertezza”. Qui ci viene in aiuto il metodo flatMap che, come visto negli stream, si occupa di schiacciare un livello di inscatolamento.

Optional<ParkingType> opt = apt.getParking().flatMap(p -> p.getParkingType());
// with opt do something..

Se il parcheggio è nullo, flatMap restituisce Optional.empty(), se invece il parcheggio è valorizzato restituisce il risultato di getParkingType.

Il metodo filter

Infine guardiamo questo codice dove abbiamo una doppia condizione, il reference deve essere non nullo e deve avere un valore specifico:

Parking parking = // assigned somewhere and not null
if (parking.getParkingType() != null && 
      (parking.getParkingType() == ParkingType.COVERED))
	score += 5;

possiamo riscrivere il tutto usando questa sintassi:

Parking parking = // assigned somewhere and not null
score += parking.getParkingType()
                .filter(pt -> pt == ParkingType.COVERED)
				.map(pt -> 5)
				.orElse(0);

Esaminiamo in dettaglio il codice: stiamo partendo da un Optional<ParkingType>. Prima il caso facile, se l’Optional è vuoto abbiamo una sequenza di operazioni che non producono alcun effetto, fino ad arrivare al metodo orElse che restituisce zero. Se invece c’è un valore all’interno dell’Optional, il metodo filter decide cosa deve andare avanti nella pipeline. Se la condizione del filtro è vera, l’Optional procede immutato nella sequenza di operazioni. Se è falsa, invece, viene sostituito da un Optional vuoto e ci rifacciamo al caso precedente.

L’esempio completo

Mettendo assieme tutti i pezzi possiamo quindi riscrivere la nostra funzione di punteggio.

public static int getScore3(ApartmentWithOptional apt) {
    int score = 0;
    if (apt != null) {
      score = apt.getNumberOfRooms() * 20;
	  
      score += apt.getParking()
	              .map(p -> p.getNumberOfPlaces() * 4)
				  .orElse(0);

      score += apt.getParking()
	              .flatMap(p -> p.getParkingType());
                  .filter(c -> c == ParkingType.COVERED)
	              .map(c -> 4)
				  .orElse(0);
	}
	  
    return score;
}

Rispetto al codice di partenza abbiamo scritto le cose in modo diverso e, inutile negarlo, ci vorrà del tempo per farci l’occhio. In fin dei conti, il passaggio dal paradigma imperativo/object oriented a quello funzionale richiede un po’ di applicazione e non è così semplice come imparare un altro linguaggio simile a Java.

Non dimentichiamo gli stream!

Gli Optional sono usati negli stream. Ad esempio, il metodo findFirst restituisce il primo elemento di uno stream attraverso un Optional, perché lo stream di partenza potrebbe essere vuoto. Facciamo un esempio utilizzando una lista di Apartment.

List<Apartment> list = new ArrayList<>();
list.add(apt23);
list.add(apt24);

list.stream()
    .filter(a -> a.getNumberOffRooms() == 3)
	.findFirst()
    .ifPresent(a -> System.out.println("Apartment found"));

Nel codice partiamo da una lista non vuota di Apartment e applichiamo un filtro. Prendiamo dallo stream risultante, che potrebbe essere vuoto, il primo elemento che è di tipo Optional<Apartment> e, come si può intuire, se l’Optional contiene qualcosa stampiamo su console un messaggio tramite il metodo ifPresent. Questo metodo prende in ingresso una functional interface (e quindi una lambda) di tipo Consumer, lo stesso tipo che abbiamo usato nel forEach spiegato in questo post.

Ci sono altri metodi di Stream che tornano un Optional: findAny che torna un qualsiasi elemento (in modo deterministico se l’esecuzione è parallela), max e min che ritornano il massimo e minimo elemento in base ad un comparatore e reduce che effettua una riduzione sugli elementi dello stream.

Conclusioni

Gli Optional non sono una esclusiva di Java, anche Scala, ad esempio, ha un concetto del tutto simile chiamato Option. In Scala, tuttavia, gli Option sono stati integrati da subito nelle librerie standard e quindi sono molto più “potenti” degli equivalenti cugini Java. Infatti, al momento, gli Optional sono usati solo dentro gli stream e non sono stati portati anche nelle altre librerie. Il motivo è semplice, far ciò avrebbe interrotto la compatibilità con il codice esistente. Un altro potenziale freno alla diffusione degli Optional consiste nella presenza di soluzioni alternative per la gestione dei null, come le annotazioni o il Null Object Pattern.

Gli Optional sono comunque semplici da capire e da utilizzare e non lasciano dubbi su cosa possa ritornare un metodo. Abbinati poi alle lambda expression, consentono di ottenere del codice compatto ed espressivo ma soprattutto NullPointerException free.

Giampaolo Trapasso

Sono laureato in Informatica e attualmente lavoro come Software Engineer in Radicalbit. Mi diverto a programmare usando Java e Scala, Akka, RxJava e Cassandra. Qui mio modesto contributo su StackOverflow e il mio account su GitHub