Guía para usar opcional en Java 8

La clase Opcional se introdujo en Java 8 para solucionar muchos de los problemas con las referencias nulas. En este artículo, nos sumergiremos en la clase, cómo se usa y mejoraremos el código con Opcionales.

Introducción

Al escribir cualquier tipo de código en Java, los desarrolladores tienden a trabajar con objetos más a menudo que con valores primitivos (int, boolean, etc.). Esto se debe a que los objetos son la esencia misma de la programación orientada a objetos: permiten que un programador escriba código abstracto de manera limpia y estructurada.

Además, cada objeto en Java puede contener un valor o no. Si lo hace, su valor se almacena en el montón y la variable que estamos usando tiene una referencia a ese objeto. Si el objeto no contiene ningún valor, el valor predeterminado es “nulo”, un marcador de posición especial que indica la ausencia de un valor.

El hecho de que cada objeto pueda volverse nulo, combinado con la tendencia natural a usar objetos en lugar de primitivos, significa que alguna pieza arbitraria de código podría (y muchas veces lo hará) dar como resultado una NullPointerException inesperada.

Antes de que se introdujera la clase ‘Opcional’ en Java 8, este tipo de errores de ‘NullPointerException’ eran mucho más comunes en la vida cotidiana de un programador de Java.

En las siguientes secciones, profundizaremos en la explicación de ‘Opcional’ y veremos cómo se puede usar para superar algunos de los problemas comunes relacionados con los valores nulos.

La clase opcional

Un Opcional es esencialmente un contenedor. Está diseñado para almacenar un valor o para estar "vacío" si el valor no existe - un reemplazo para el valor nulo. Como veremos en algunos ejemplos posteriores, este reemplazo es crucial ya que permite la verificación nula implícita para cada objeto representado como ‘Opcional’.

Esto significa que la comprobación nula explícita ya no es necesaria desde el punto de vista del programador: el propio lenguaje la impone.

Crear opcionales

Echemos un vistazo a lo fácil que es crear instancias de ‘Opcional’ y envolver objetos que ya tenemos en nuestras aplicaciones.

Usaremos nuestra clase personalizada para esto, la clase Spaceship:

1
2
3
4
5
6
public class Spaceship {
    private Engine engine;
    private String pilot;

    // Constructor, Getters and Setters
}

Y nuestro ‘Motor’ se parece a:

1
2
3
4
5
public class Engine {
    private VelocityMonitor monitor;

    // Constructor, Getters and Setters
}

Y además, tenemos la clase VelocityMonitor:

1
2
3
4
5
public class VelocityMonitor {
    private int speed;

    // Constructor, Getters and Setters
}

Estas clases son arbitrarias y solo sirven para hacer un punto, no hay una implementación real detrás de ellas.

de()

El primer enfoque para crear ‘Opcionales’ es usar el método ‘.of()’, pasando una referencia a un objeto no nulo:

1
2
Spaceship falcon = new Spaceship();
Optional<Spaceship> optionalFalcon = Optional.of(falcon);

Si falcon fuera null, el método .of() lanzaría una NullPointerException.

Sin ‘Opcional’, intentar acceder a cualquiera de los campos o métodos de ‘falcon’ (asumiendo que es ’null’), sin realizar una verificación de nulo, resultaría en un bloqueo del programa.

Con ‘Opcional’, el método ‘.of()’ detecta el valor ’nulo’ y lanza la excepción ‘NullPointerException’ de inmediato, lo que podría bloquear el programa.

Si el programa falla en ambos enfoques, ¿por qué molestarse en usar ‘Opcional’?

El programa no fallaría en algún lugar más profundo del código (al acceder a falcon), sino en el primer uso (inicialización) de un objeto nulo, minimizando el daño potencial.

deAnulable()

Si se permite que falcon sea un null, en lugar del método .of(), usaríamos el método .ofNullable(). Realizan lo mismo si el valor no es null. La diferencia es obvia cuando la referencia apunta a null, en cuyo caso, el método .ofNullable() se desprecia perfectamente con esta pieza de código:

1
2
Spaceship falcon = null;
Optional<Spaceship> optionalFalcon = Optional.ofNullable(falcon);

vacío()

Y finalmente, en lugar de envolver una variable de referencia existente (null o no null), podemos crear un valor null en el contexto de un Optional. Es como un contenedor vacío que devuelve una instancia vacía de Opcional:

1
Optional<Spaceship> emptyFalcon = Optional.empty();

Comprobación de valores

Después de crear ‘Opcionales’ y empaquetar información en ellos, es natural que queramos acceder a ellos.

Sin embargo, antes de acceder, debemos verificar si hay algún valor o si los ‘Opcionales’ están vacíos.

está presente()

Dado que la captura de excepciones es una operación exigente, sería mejor utilizar uno de los métodos de la API para comprobar si el valor existe antes de intentar acceder a él y modificar el flujo si no es así.

Si es así, entonces se puede usar el método .get() para acceder al valor. Sin embargo, más sobre ese método en las últimas secciones.

Para verificar si el valor está presente dentro de un Opcional, usamos el método .isPresent(). Esto es esencialmente un reemplazo para el cheque null de los viejos tiempos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// Without Optional
Spaceship falcon = hangar.getFalcon();
if (falcon != null) {
    System.out.println(falcon.get());
} else {
    System.out.printn("The Millennium Falcon is out and about!");
}

// With Optional
Optional<Spaceship> optionalFalcon = Optional.ofNullable(hangar.getFalcon());
if (optionalFalcon.isPresent()) {
    System.out.println(falcon.get());
} else {
    System.out.println("The Millennium Falcon is out and about!");
}

Dado que falcon tampoco puede estar en el hangar, también podemos esperar un valor null, por lo que se usa .ofNullable().

si está presente()

Para facilitar aún más las cosas, Opcional también contiene un método condicional que omite por completo la verificación de presencia:

1
2
Optional<Spaceship> optionalFalcon = Optional.ofNullable(hangar.getFalcon());
optionalFalcon.ifPresent(System.out::println);

Si un valor está presente, los contenidos se imprimen a través de una Referencia del método. Si no hay valor en el contenedor, no pasa nada. Sin embargo, es posible que aún desee utilizar el enfoque anterior si desea definir una declaración else {}.

Esto refleja lo que mencionamos anteriormente cuando dijimos que las verificaciones null con Optional son implícitas y aplicadas por el sistema de tipos.

esta vacio()

Otra forma de verificar un valor es usar .isEmpty(). Esencialmente, llamar a Optional.isEmpty() es lo mismo que llamar a !Optional.isPresent(). No hay ninguna diferencia particular que exista:

1
2
3
4
5
6
Optional<Spaceship> optionalFalcon = Optional.ofNullable(hangar.getFalcon());
if (optionalFalcon.isEmpty()) {
    System.out.println("Please check if the Millennium Falcon has returned in 5 minutes.");
} else {
    optionalFalcon.doSomething();
}

Comprobaciones nulas anidadas

Nuestra clase Spaceship, como se definió anteriormente, tiene un atributo Engine, que tiene un atributo VelocityMonitor.

Supongamos ahora que queremos acceder al objeto monitor de velocidad y obtener la velocidad actual de la nave espacial, teniendo en cuenta que todos estos valores podrían ser potencialmente “nulos”.

Obtener la velocidad podría ser algo como esto:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
if (falcon != null) {
    Engine engine = falcon.getEngine();
    if (engine != null) {
        VelocityMonitor monitor = engine.getVelocityMonitor();
        if (monitor != null) {
            Velocity velocity = monitor.getVelocity();
            System.out.println(velocity);
        }
    }
}

El ejemplo anterior muestra lo tedioso que es realizar dichas comprobaciones, sin mencionar la cantidad de código repetitivo necesario para hacer posibles las comprobaciones en primer lugar.

Una solución alternativa usando ‘Opcional’ sería:

1
2
3
4
Velocity velocity = falcon
    .flatMap(Spaceship::getEngine)
    .flatMap(Engine::getVelocityMonitor)
    .map(VelocityMonitor::getVelocity);

Nota: ¿No estás seguro de lo que está pasando arriba? Consulta la explicación a continuación para conocer los detalles.

Con este tipo de enfoque, no se necesitan controles explícitos. Si alguno de los objetos contiene un Opcional vacío, el resultado final también será un Opcional vacío.

Para hacer que las cosas funcionen así, necesitamos modificar nuestras definiciones existentes de las clases Spaceship y Engine:

1
2
3
4
5
6
public class Spaceship {
    private Optional<Engine> engine;
    private String pilot;

    // Constructor, Getters and Setters
}
1
2
3
4
5
public class Engine {
    private Optional<VelocityMonitor> monitor;

    // Constructor, Getters and Setters
}

Lo que hemos cambiado son las definiciones de los atributos: ahora están envueltos dentro de objetos ‘Opcionales’ para hacer posible este tipo de solución alternativa.

Esto puede parecer un poco tedioso al principio, pero si se planea desde el principio, se necesita casi la misma cantidad de esfuerzo para escribirlo.

Además, tener un atributo Opcional en lugar de un objeto normal refleja el hecho de que el atributo podría o no existir. Observe cómo esto es bastante útil ya que no tenemos significados semánticos de este tipo con definiciones de atributos regulares.

Ejemplo de explicación

En esta sección, nos tomaremos un poco de tiempo para explicar el ejemplo anterior con flatMaps y maps. Si lo entiende sin más explicaciones, no dude en omitir esta sección.

La primera llamada al método se realiza en falcon, que es de tipo Optional<Spaceship>. Llamar al método getEngine devuelve un objeto de tipo Optional<Engine>. Al combinar estos dos tipos, el tipo del objeto devuelto se convierte en Opcional<Opcional<Motor>>.

Dado que nos gustaría ver este objeto como un contenedor Engine y realizar más llamadas en él, necesitamos algún tipo de mecanismo para "despegar" la capa exterior Optional.

Tal mecanismo existe y se llama flatMap. Este método API combina las operaciones map y flat aplicando primero una función a cada uno de los elementos y luego aplanando el resultado en un flujo de un nivel.

El método mapa, por otro lado, solo aplica una función sin aplanar la transmisión. En nuestro caso, el uso de map y flatMap nos daría Optional<Optional<Engine>> y Optional<Engine> respectivamente.

Por lo tanto, llamar a flatMap en un objeto de tipo Opcional generaría un Opcional de un nivel, lo que nos permitiría usar varias llamadas a métodos similares en una sucesión.

Esto finalmente nos deja con Optional<Engine>, que queríamos en primer lugar.

Resultados alternativos

.si no()

El ejemplo anterior se puede ampliar aún más utilizando el método orElse(T other). El método devolverá el objeto ‘Opcional’ sobre el que se llama solo si hay un valor contenido en él.

Si Opcional está vacío, el método devuelve el valor otro. Esta es esencialmente una versión Opcional del operador ternario:

1
2
3
4
5
// Ternary Operator
Spaceship falcon = maybeFalcon != null ? maybeFalcon : new Spaceship("Millennium Falcon");

// Optional and orElse()
Spaceship falcon = maybeFalcon.orElse(new Spaceship("Millennium Falcon"));

Al igual que con el método ifPresent(), este tipo de enfoque aprovecha las expresiones lambda para hacer que el código sea más legible y menos propenso a errores.

.oElseGet()

En lugar de proporcionar el valor “otro” directamente como argumento, podemos usar a
Proveedor en su lugar. La diferencia entre .orElse() y .orElseGet(), aunque tal vez no sea evidente a primera vista, existe:

1
2
3
4
5
// orElse()
Spaceship falcon = maybeFalcon.orElse(new Spaceship("Millennium Falcon"));

// orElseGet()
Spaceship falcon = maybeFalcon.orElseGet(() -> new Spaceship("Millennium Falcon"));

Si maybeFalcon no contiene un valor, ambos métodos devolverán una nueva Spaceship. En este caso, su comportamiento es el mismo. La diferencia queda clara si maybeFalcon no contiene un valor.

En el primer caso, el objeto nueva nave espacial no se devolverá pero se creará. Esto sucederá independientemente de si el valor existe o no. En el segundo caso, la nueva nave espacial se creará solo si maybeFalcon no contiene un valor.

Es similar a cómo do-while hace la tarea independientemente del bucle while, al menos una vez.

Esto puede parecer una diferencia insignificante, pero se vuelve bastante importante si la creación de naves espaciales es una operación exigente. En el primer caso, siempre estamos creando un nuevo objeto, incluso si nunca se usará.

.orElseGet() debe preferirse en lugar de .orElse() en tales casos.

.orElseThrow()

En lugar de devolver un valor alternativo (como hemos visto en las dos secciones anteriores), podemos lanzar una excepción. Esto se logra con el método .orElseThrow() que, en lugar de un valor alternativo, acepta un proveedor que devuelve la excepción en caso de que sea necesario lanzarla.

Esto puede ser útil en casos donde el resultado final es de gran importancia y no debe estar vacío. Lanzar una excepción en este caso podría ser la opción más segura:

1
2
// Throwing an exception
Spaceship falcon = maybeFalcon.orElseThrow(NoFuelException::new);

Obtener valores de {#gettingvaluesfromopcional} opcional

.obtener()

Después de ver muchas formas diferentes de verificar y acceder al valor dentro de ‘Opcional’, echemos un vistazo a una forma final de obtener el valor que también usa algunos de los métodos mostrados anteriormente.

La forma más sencilla de acceder a un valor dentro de un ‘Opcional’ es con ‘.get()’. Este método devuelve el valor presente o lanza una NoSuchElementException si el valor está ausente:

1
2
3
4
5
6
Optional<Spaceship> optionalFalcon = Optional.ofNullable(hangar.getFalcon());
if (falcon.isPresent()) {
    Spaceship falcon = optionalFalcon.get()

    // Fly the falcon
}

Como era de esperar, el método .get() devuelve una instancia no nula de la clase Spaceship y la asigna al objeto falcon.

Conclusión

Opcional se introdujo en Java como una forma de solucionar los problemas con las referencias nulas. Antes de Opcional, cada objeto podía contener un valor o no (es decir, ser nulo).

La introducción de ‘Opcional’ esencialmente impone la comprobación de ’nulos’ por parte del sistema de tipos, lo que hace innecesario realizar dichas comprobaciones manualmente.

Este fue un gran paso tanto para mejorar el lenguaje como su facilidad de uso al agregar una capa adicional de verificación de tipos. El uso de este sistema en lugar de la anticuada verificación null permite escribir código claro y conciso sin la necesidad de agregar repeticiones y realizar comprobaciones agotadoras a mano. a mano.

Licensed under CC BY-NC-SA 4.0