Cansat de les excepcions de punter nul? Avaluï la possibilitat d’usar Optional de Java SE 8.

Un home molt savi va dir alguna vegada que no s’és un veritable programador de Java fins que no s’ha enfrontat una excepció de punter nul. Bromes a part, la referència nul·la dóna origen a molts problemes perquè sovint es fa servir per denotar l’absència d’un valor. Java SE 8 presenta una nova classe anomenada java.util.Optional que pot pal·liar alguns d’aquests problemes.

Comencem amb un exemple per veure els perills d’utilitzar null. Pensem, per exemple, en una estructura d’objectes niats per representar Computer, com s’il·lustra a la Figura 1.

Java8 -optional- fig.1

Figura 1: Estructura imbricada per representar Computer

Quins problemes pot presentar el següent codi?

String version = computer.getSoundcard().getUSB().getVersion();

el codi sembla bastant lògic. No obstant això, moltes computadores (per exemple, la Raspberry Pi) es distribueixen sense targeta de so. Llavors, quin és el resultat de getSoundcard ()?

Una (mala) pràctica habitual és tornar la referència nul·la per indicar l’absència de targeta de so. Lamentablement, això vol dir que la crida a getUSB () intentarà tornar el port USB d’una referència nul·la; en conseqüència, es llançarà una excepció NullPointerException en temps d’execució i el programa deixarà de executar-se. Imagini que el seu programa s’estigués executant en l’equip d’un client: què diria aquest client si el programa, de sobte, fallés?

Per proporcionar una mica de context històric, Tony Hoare -un dels gegants de les ciències de la computació- va escriure: “el dic el meu error dels mil milions de dolars: l’invent de la referència nul·la en 1965. No vaig poder resistir la temptació d’inserir una referència nul·la. Era tan fàcil implementar-la …”.

Què es pot fer per evitar les excepcions de punter nul no intencionals? Es pot adoptar una actitud defensiva i afegir comprovacions per evitar les referències nul·les, com es mostra en el Llistat 1:

String version = "UNKNOWN";if(computer != null){ Soundcard soundcard = computer.getSoundcard(); if(soundcard != null){ USB usb = soundcard.getUSB(); if(usb != null){ version = usb.getVersion(); } }}

Llistat 1

No obstant això, és fàcil veure que de seguida el codi de l’Llistat 1 comença a perdre elegància a causa de les comprovacions niuades. Per desgràcia, necessitem molt codi repetitiu per assegurar-nos de no obtenir un error NullPointerException. A més, resulta molest que aquestes comprovacions interfereixin amb la lògica de negocis. De fet, redueixen la llegibilitat general de el programa.

És més, es tracta d’un procés propens a errors: ¿què passaria si s’oblidés de comprovar que una propietat pot resultar nul·la? En el present article, argumentaré que utilitzar null per representar l’absència d’un valor constitueix un enfocament erroni. El que necessitem, és una millor manera de modelar l’absència i la presència d’un valor.

Per proporcionar cert context a l’anàlisi, examinem què ofereixen altres llenguatges de programació.

Quines són les alternatives a l’ús de null?

Els llenguatges com Groovy compten amb un operador de navegació segura representat per “?.” per sortejar sense riscos possibles referències nul·les. (Cal notar que C # aviat comptarà també amb aquest operador, i que s’havia proposat incloure-ho en Java SE 7, encara que no va ser possible arribar a fer-ho en aquesta versió.) Funciona de la següent manera:

String version = computer?.getSoundcard()?.getUSB()?.getVersion();

En aquest cas, a la variable versió se li assignarà un valor null si computer és null o getSoundcard () retorna null o getUSB () retorna null. No cal introduir condicions niuades complexes per comprovar la presència de null. A més, Groovy també compta amb l’operador Elvis “?:” (Si el mira de costat, reconeixerà el famós pentinat d’Elvis), que pot utilitzar per a casos senzills quan es requereix un valor per omissió. En el següent exemple, si l’expressió que utilitza l’operador de navegació segura retorna null, es torna el valor per defecte “UNKNOWN”; en el cas contrari, es retorna l’etiqueta amb la versió disponible.

String version = computer?.getSoundcard()?.getUSB()?.getVersion() ?: "UNKNOWN";

Altres llenguatges funcionals, com Haskell i Scala, adopten una visió diferent. Haskell inclou un tipus Maybe, que, bàsicament, encapsula un valor opcional. Un valor de l’tipus Maybe pot contenir un valor d’un tipus donat o res. No existeix el concepte de referència nul·la. Scala compta amb un constructe similar denominat Option per encapsular la presència o absència d’un valor de l’tipus T. Després és necessari comprovar de manera explícita si un valor està present o absent usant operacions disponibles en el tipus Option, el que torna obligatòria la “comprovació de null “. Ja no és possible “oblidar-se de comprovar” perquè el sistema de tipus no ho permet.Bé, ens desviem una mica del tema i tot això sona bastant abstracte. Potser s’estiguin preguntant: “Llavors, què ofereix Java SE 8?”.

Optional en poques paraules

Java ES 8 inclou una classe nova anomenada java.util.Optional < T >, inspirada en Haskell i Scala. Es tracta d’una classe que encapsula un valor opcional, com es mostra en el Llistat 2, inclòs a continuació, i en la Figura 2. Optional pot considerar-se un contenidor de valor únic que o bé conté un valor o no el conté (en aquest cas, es diu que està “buit”), com es mostra a la Figura 2. Java8 -optional- fig.2

Figura 2: Targeta de so opcional

Podem modificar el nostre model i usar Optional, com s’observa en la Llista 2:

public class Computer { private Optional<Soundcard> soundcard; public Optional<Soundcard> getSoundcard() { ... } ...} public class Soundcard { private Optional<USB> usb; public Optional<USB> getUSB() { ... } } public class USB{ public String getVersion(){ ... }}

Llistat 2

En la Llista 2, és evident immediatament que un ordinador pot o no tenir targeta de so (la targeta de so és opcional). A més, la targeta de so pot comptar opcionalment amb port USB. Es tracta d’una millora respecte de el model anterior, ja que aquest nou model reflecteix amb claredat si es permet que un valor determinat estigui absent. Cal notar que en biblioteques com Guava es troben disponibles possibilitats similars. Però què es pot fer en realitat amb un objecte Optional < Soundcard >? Després de tot, vostè vol obtenir el nombre de versió de l’port USB. En poques paraules, la classe Optional inclou mètodes que permeten ocupar explícitament dels casos en què un valor està present o absent. No obstant això, l’avantatge en comparació amb les referències nul·les radica que la classe Optional obliga a pensar en el cas en què el valor no estigui present. Com a conseqüència, és possible prevenir les excepcions de punter nul no intencionals. És important assenyalar que l’objectiu de la classe Optional no és reemplaçar totes les referències nul·les, sinó que el seu propòsit consisteix a ajudar a dissenyar rutines API més comprensibles, tals que amb només llegir la signatura d’un mètode sigui possible saber si pot esperar-se la devolució d’un valor opcional. Si aquest és el cas, ens veiem obligats a “desencapsular” la classe Optional per actuar davant l’absència d’un valor.

Patrons per a l’adopció de Optional

Suficient explicació: passem a l’ codi. En primer lloc, veurem com reescriure patrons típics de comprovació de null utilitzant Optional. A l’concloure el present article, vostè sabrà com utilitzar Optional per reescriure el codi que mostra el Llistat 1, en el qual es realitzaven diverses comprovacions de null niades (veure a baix):

String name = computer.flatMap(Computer::getSoundcard) .flatMap(Soundcard::getUSB) .map(USB::getVersion) .orElse("UNKNOWN");

Nota: Assegureu-vos de repassar abans la sintaxi de les referències a mètodes i expressions lambda de Java SE 8 (veure “Java 8: Lambdes”) així com els conceptes de canalització (pipelining) de streams (veure “Processing Data with Java sE 8 streams”).

Creació d’objectes Optional

En primer lloc, com es creen objectes Optional? Hi ha diversos modes: Aquest és un Optional buit:

Optional&lt;Soundcard&gt; sc = Optional.empty();

I aquest és un Optional amb un valor no nul:

Si soundcard fos nul, es llançaria immediatament una excepció NullPointerException (en lloc d’obtenir un error latent quan s’intenti accedir a les propietats de soundcard). Així mateix, utilitzant ofNullable, és possible crear un objecte Optional que pot contenir un valor nul:

Optional<Soundcard> sc = Optional.ofNullable(soundcard);

Si soundcard fos nul, l’objecte Optional resultant estaria buit.

Fer alguna cosa davant la presència d’un valor

Ara que comptem amb un objecte Optional, podem recórrer als mètodes disponibles per ocupar-nos de manera explícita de la presència o absència de valors. En lloc de veure’ns obligats a recordar fer una comprovació de null, de la manera següent:

SoundCard soundcard = ...;if(soundcard != null){ System.out.println(soundcard);}

podem usar el mètode ifPresent ( ) com es veu a continuació:

Optional<Soundcard> soundcard = ...;soundcard.ifPresent(System.out::println);

Ja no necessitem efectuar una comprovació de null explícita: el sistema de tipus mateix s’ocupa d’executar-la. Si l’objecte Optional estigués buit, no s’imprimiria res. També podem utilitzar el mètode isPresent () per esbrinar si hi ha un valor en un objecte Optional. A més, hi ha un mètode get () que retorna el valor contingut en l’objecte Optional, si estigués present. En cas contrari, llança una excepció NoSuchElementException. És possible combinar tots dos mètodes per prevenir excepcions, com es mostra a continuació:

if(soundcard.isPresent()){ System.out.println(soundcard.get());}

No obstant això, no és aquest l’ús recomanat de Optional (no representa una millora significativa respecte de les comprovacions de null niuades); a més, hi ha alternatives més idiomàtiques, que explorarem més endavant.

Valors predeterminats i accions

Un patró típic consisteix en retornar un valor per defecte si es determina que el resultat d’una operació és nul. En general, per aconseguir aquest objectiu pot emprar l’operador ternari:

Soundcard soundcard = maybeSoundcard != null ? maybeSoundcard : new Soundcard("basic_sound_card");

Si s’usa un objecte Optional, és possible reescriure el codi anterior emprant el mètode orElse (), que proporciona un valor per omissió si Optional està buit:

Soundcard soundcard = maybeSoundcard.orElse(new Soundcard("defaut")); 

de manera similar, pot utilitzar-se el mètode orElseThrow (), que, en lloc de proporcionar un valor per defecte en cas que Optional estigués buit, llança una excepció:

Soundcard soundcard = maybeSoundCard.orElseThrow(IllegalStateException::new);

Rebuig de certs valors amb l’ús de l’mètode filter

sovint, cal trucar un mètode d’un objecte i comprovar alguna propietat. Per exemple, pot ser necessari verificar si el port USB és d’una versió determinada. Per fer-ho sense riscos, cal comprovar primer si la referència que apunta a un objecte USB és nul·la i cridar, després, el mètode getVersion (), de la següent manera:

USB usb = ...;if(usb != null && "3.0".equals(usb.getVersion())){ System.out.println("ok");}

És possible reescriure aquest patró utilitzant el mètode filter per a un objecte Optional, com es mostra a continuació:

Optional<USB> maybeUSB = ...;maybeUSB.filter(usb -> "3.0".equals(usb.getVersion()) .ifPresent(() -> System.out.println("ok"));

El mètode filter pren un predicat com a argument. Si hi ha un valor en l’objecte Optional i aquest valor compleix amb el predicat, el mètode filter retorna aquest valor; en cas contrari, retorna un objecte Optional buit. És possible que hi hagi trobat un patró similar si ha utilitzat el mètode filter amb la interfície Stream.

Extracció i transformació de valors amb el mètode map

Un altre patró freqüent consisteix a extreure informació de un objecte. Per exemple, pot passar que es vulgui extreure l’objecte USB d’un objecte Soundcard i comprovar, a continuació, si és de la versió correcta. El codi típic seria:

if(soundcard != null){ USB usb = soundcard.getUSB(); if(usb != null && "3.0".equals(usb.getVersion()){ System.out.println("ok"); }}

És possible reescriure aquest patró de “comprovar null i extreure” (en aquest cas, l’objecte Soundcard) usant el mètode map.

Optional<USB> usb = maybeSoundcard.map(Soundcard::getUSB);

Hi ha un paral·lel directe amb el mètode map usat amb streams. Allà, es passa una funció a l’mètode map, que aplica aquesta funció a cada element d’un stream. No obstant això, si el stream està buit no passa res. El mètode map de la classe Optional fa el mateix: la funció que es passa com a argument (en aquest cas, una referència a un mètode per extreure el port USB) “transforma” el valor contingut en Optional, mentre que res passa si Optional està buit. Finalment, podem combinar el mètode map amb el mètode filter per rebutjar un port USB la versió no sigui 3.0:

maybeSoundcard.map(Soundcard::getUSB) .filter(usb -> "3.0".equals(usb.getVersion()) .ifPresent(() -> System.out.println("ok"));

Genial : el nostre codi comença a acostar-se a l’objectiu buscat, sense comprovacions de null explícites que s’interposen en el camí.

cascada d’objectes Optional amb el mètode flatMap

Ja hem vist alguns patrons que poden refactorizarse per utilitzar Optional. Ara, com podem escriure el següent codi de manera segura?

String version = computer.getSoundcard().getUSB().getVersion();

Cal notar que l’única funció d’aquest codi és extreure un objecte d’un altre, exactament el propòsit de l’mètode map. En línies anteriors, modifiquem el nostre model de manera tal que Computer tingués Optional < Soundcard > i Soundcard tingués Optional < USB >, de manera que hauria de ser possible escriure el següent:

String version = computer.map(Computer::getSoundcard) .map(Soundcard::getUSB) .map(USB::getVersion) .orElse("UNKNOWN");

Lamentablement, no és possible compilar aquest codi. Per què? La variable Computer és de tipus Optional < Computer >, de manera que és correcte anomenar el mètode map. No obstant això, getSoundcard () retorna un objecte de tipus Optional < Soundcard >, el que significa que el resultat de l’operació map és un objecte de tipus Optional < Optional < Soundcard > >. Com a conseqüència, l’anomenat a getUSB () no és vàlid perquè el Optional exterior conté com a valor un altre Optional que, per descomptat, no admet el mètode getUSB (). La Figura 3 il·lustra l’estructura imbricada de Optional que s’obtindria. Java8 -optional- fig.3

Figura 3: Un Optional de dos nivells

Com es pot resoldre el problema? Un cop més, podem recórrer a un patró que potser vostè hagi fet servir abans amb streams: el mètode flatMap. Amb els streams, el mètode flatMap pren una funció com a argument, el que retorna un nou stream. Aquesta funció s’aplica a cada element de l’stream, la qual cosa tindria com a resultat un stream d’streams.No obstant això, l’efecte de flatMap consisteix a reemplaçar cada stream que es genera pel contingut de l’stream de què es tracti. En altres paraules, tots els streams que genera la funció s’amalgamen o “s’aplanen” en un únic stream. El que necessitem en aquest cas és una cosa similar, però busquem “aplanar” 1 Optional de dos nivells i obtenir, en canvi, un sol nivell.

Bé, tenim una bona notícia: Optional també admet un mètode flatMap . El seu propòsit és aplicar la funció de transformació a la valor d’un Optional (tal com passa amb l’operació map) i, a continuació, “aplanar” el Optional de dos nivells per obtenir un únic nivell. La Figura 4 il·lustra la diferència entre map i flatMap quan la funció de transformació retorna un objecte Optional.

Java8 -optional- fig.4

Figura 4: Comparació de l’ús de map i flatMap amb Optional

Llavors, perquè el codi sigui correcte, hem de reescriure-de la següent manera usant flatMap:

String version = computer.flatMap(Computer::getSoundcard) .flatMap(Soundcard::getUSB) .map(USB::getVersion) .orElse("UNKNOWN");

El primer flatMap garanteix que es retorni Optional < Soundcard > en lloc de Optional < Optional < Soundcard > >, i el segon flatMap aconsegueix el mateix objectiu amb la devolució de Optional < USB >. Cal notar que en el cas de l’tercer anomenat, només es necessita map () perquè getVersion () retorna una String en lloc d’un objecte Optional.

Genial! Hem avançat moltíssim: passem d’escriure molestes comprovacions de null niades a escriure un codi declaratiu que és llegible, admet composició i es troba millor protegit de les excepcions de punter nul.

Conclusió

en el present article, hem abordat l’adopció de la nova classe java.util.Optional < T > de Java ES 8. el propòsit de Optional no rau en reemplaçar totes les referències nul·les de el codi, sinó en ajudar a dissenyar millors rutines API en les que, mitjançant la lectura de la signatura d’un mètode, els usuaris sàpiguen si han o no esperar un valor opcional. A més, Optional obliga a “desencapsular ‘” un Optional per tal d’actuar davant l’absència d’un valor; com a resultat, es preveu la presència d’excepcions de punter nul no intencionals en el codi.

Informació addicional

  • Capítol 9, “Optional: a better alternative to null” ( Optional, una alternativa millor que null), en Java 8 in Action: lambdes, streams, and Functional-style Programming (Java agost en acció: lambdes, streams i programació funcional)
  • “Monadic Java” (Java monádico) per Mario Fusco
  • “Processing Data with Java SE 8 streams” (Processament de dades amb streams de Java SE 8)

Agraïments

Agraeixo a Alan Mycroft i Mario Fusco per emprendre l’aventura d’escriure Java 8 in Action: Lambdes, Streams, and Functional-style Programming juntament amb mi.

Raoul-Gabriel URMA (@raoulUK) està acabant el seu doctorat en Ciències de la Computació a la Universitat de Cambridge, on desenvolupa la seva recerca en llenguatges de programació. És coautor de Java 8 in Action: Lambdes, Streams, and Functional-style Programming, que serà publicat pròximament per Manning. A més, participa habitualment com a expositor a conferències sobre Java de primera línia (per exemple Devoxx i FOSDEM) i s’exerceix com a instructor. Així mateix, ha treballat en diverses empreses prestigioses, entre elles l’equip Python de Google, el grup Java Platform d’Oracle, eBay i Goldman Sachs, així com en diversos projectes de nous iniciatives.

Leave a Comment

L'adreça electrònica no es publicarà. Els camps necessaris estan marcats amb *