Optionals en Java - Parte 2. Evitando validaciones 'null' de 'if' anidados.
Java

Optionals en Java - Parte 2. Evitando validaciones 'null' de 'if' anidados.

Silverio Martínez García
Silverio Martínez García

En el artículo anterior sobre Optionals en Java hemos visto como y cuando se deben utilizar.

En este artículo veremos una de las mayores ventajas de los 'Optionals' a la hora de acceder a campos de objetos que pueden valer 'null' y evitarnos el tener que comprobar si cada referencia vale 'null' con 'if' anidados.

En otros lenguajes de programación como C# o TypeScript esto se puede hacer de forma sencilla ya que se hace uso del Null Conditional Operator.
Pero en Java, para obtener la misma funcionalidad, de momento tenemos que hacer uso de los 'Optionals'.

Entrando en contexto:

Normalmente en Java cuando queremos acceder al campo de un objeto hay que comprobar primero que dicho objeto no valga 'null'. La cosa se complica cuando queremos acceder a un campo que está dentro del campo de otro objeto y así sucesivamente:

Al final nuestro código se convierte en un montón de 'ifs' anidados cuando en C# por ejemplo, el mismo código sería de la siguiente manera:

Si alguno de los campos vale 'null' se asigna 'null' directamente a 'stateName' y no se siguen evaluando los demás.

Este resultado es el mismo que con el ejemplo de Java pero con mucho menos código y mucho más legible.

Vamos a ver ahora como se consigue esto en Java y de paso encontrándole un poco más de utilidad a los 'Optionals'.
Partiremos del mismo proyecto de tests que usamos en el artículo anterior con nuestros modelos de datos y el repositorio.
Puedes descargar el código fuente desde GitHub.

Empezamos a picar código:

Person.java

Address.java

State.java

PersonRepository.java


Evitando 'Null Validations' anidadas:

Vamos a ver una serie de tests para recuperar el valor de un campo sin tener que utilizar 'if' anidados para comprobar valores 'null':

En este test primero se crea un objeto 'Person' cuyo campo 'Person.address' vale 'null'.
Para comprobar que se ha creado correctamente el objeto, se intenta acceder a dicho campo y como vale 'null' se lanza una 'NullPointerException'.

A continuación se crea un 'Optional' a partir de nuestro objeto 'Person'.
Se utiliza 'map()' para intentar acceder al campo 'Person.address.street' de forma segura sin tener que comprobar valores 'null'.
La función 'map()' acepta como parámetro una lambda de tipo 'Function' que devolverá un campo del objeto actual que a su vez se envuelve dentro de un 'Optional' para poder seguir evaluándolo.
Si el campo devuelto vale 'null', la función 'map()' devolverá un 'Optional.empty'.

La línea de código '.map(person -> person.getAddress())' lo que haría sería devolver un 'Optional<Address>' con el valor del campo 'person.getAddress()'. Pero como en nuestro caso 'person.getAddress()' vale 'null', la función 'map()' devuelve un 'Optional.empty'.

La siguiente línea '.map(address -> address.getStreet())' no se llega a ejecutar ya que se está ejecutando sobre un 'Optional.empty' y salta a la siguiente función sin lanzar NullPointerException.

En la última línea '.orElseGet(() -> null);' lo que hacemos es indicar el valor final que se devolverá en caso de recibir un 'Optional.empty'. En nuestro caso hemos indicado que se devuelva 'null' pero se prodría haber indicado otro valor o incluso llamar a un método que nos devuelva el valor necesario.

En este otro test se vuelve a intentar recuperar el campo 'Person.getAddress.getStreet()' pero ahora con éxito, ya que partimos de un objeto 'Person' con valor para su campo 'address'.

Primero se crea el objeto 'Person' y se comprueba que 'address' no vale 'null' recuperando el campo 'street'.

A continuación se crea un 'Optional' a partir de nuestro objeto 'Person'.
Volvemos a utilizar 'map()' para obtener el valor de cada campo de forma segura evitando 'NullPointerExceptions'.

En la línea '.map(Person::getAddress)' se devuelve un 'Optional<Address>' con el valor del campo 'Person.getAddress()'.
Fíjate como en este caso en vez de envíar como parámetro al método 'map()' una lambda, se está enviando directamente un 'method reference', pero el efecto es justamente el mismo que en el test anterior.

La línea '.map(Address::getStreet)' en este caso sí se ejecuta porque recibe un 'Optional<Address>' que no está vacío.
En la lambda se recibe el objeto de tipo 'Address' y se devuelve un 'Optional<String>' con el valor del campo 'Address.street' correspondiente.

La línea '.orElseGet(() -> null);' en este caso no llega a ejecutarse, porque no recibe un 'Optional.empty'.
Se devuelve directamente el valor del campo 'Person.getAddress.getStreet()'.

Vamos a ver otro par de tests añadiendo un campo más, ahora intentaremos obtener el valor de 'Person.getAdress().getState().getName()' cuando 'state' vale 'null' y cuando tiene valor.
La explicación de los tests es la misma, simplemente se añade un método adicional 'map()' al 'Optional':

Conclusión:

En este artículo se comprueba como se puede obtener el valor del campo 'Person.getAdress().getStreet()' y 'Person.getAdress().getState().getName()' sin utilizar comprobación de 'null' ni 'if' anidados.

Si lo comparamos con el Null Conditional Operator de C#, en Java con 'Optional' tenemos un mecanismo para que en caso de que algún campo intermedio valga 'null' poder devolver el valor que nosotros queramos.
En C# se devolvería directamente 'null' y para poder devolver otro valor habría que utilizar un operador a mayores.

Enlaces de interés: