Objetos Opcionales con Java 8


SurpriseBoxEn el presente artículo se presentan varios ejemplos sobre cómo usar los nuevos objetos opcionales ahora disponibles en Java 8 y se hacen comparaciones con enfoques similares en otros lenguajes de programación, particularmente el lenguaje programación funcional SML y el lenguaje de programación basado en el JVM llamado Ceylon, este último actualmente en desarrollo por Red Hat.

Es importante resaltar que la introducción de los objetos opcionales ha sido sujeto de debate. En este artículo se presenta una perspectiva del problema que éstos pretenden resolver y se hace un esfuerzo por mostrar argumentos a favor y en contra de su uso. El artículo se inclina ligeramente hacia la opinión de que los objetos opcionales son de gran valor en ciertos escenarios, sin embargo, cada quien tiene derecho a una opinión e idealmente este artículo provee suficiente información para que sus lectores se formen una opinión bien informada sobre este asunto. Sobre todo las comparaciones con las estrategias que algunos lenguajes de programación (modernos y no tan modernos) han adoptado para lidiar con el problema proveen una base sólida para considerar que el tema es digno de profunda consideración.

Sobre el Tipo Null

En Java usamos referencias para obtener acceso a los objetos, y cuando no tenemos un objeto específico al que nuestra referencia deba apuntar entonces le asignamos un valor nulo para implicar la ausencia de valor.

Es interesante que en Java null es en realidad un tipo, uno especial: no tiene nombre, no podemos declarar variables de su tipo, o convertir referencias de su tipo a otros tipos (casting). De hecho existe un solo valor con el que se puede asociar (la literal null), y a diferencia de otros tipos en Java, una referencia de tipo null puede ser con seguridad asignada a cualquier otro tipo de referencia (Véase JLS 3.10.7 y 4.1).

EL uso de null es tan común que raras veces meditamos en cómo afecta la manera como programamos. Por ejemplo, null es el valor predeterminado de las referencias que son variables miembro en nuestros objetos, y los programadores, en general, inicializamos a null toda referencia para la cual no tenemos otro valor inicial que darle. Las usamos en todas partes para implicar que, habiendo llegado a cierto punto, no sabemos o no tenemos un valor que dar a una referencia.

El Problema de las Referencias Nulas

Ahora bien, el problema con las referencias nulas es que si tratamos de utilizarlas pensando que en realidad apuntan a algún objeto entonces nos ocurre el bien conocido y siempre temido NullPointerException.

Cuando trabajamos con una referencia proveniente de un contexto diferente de aquel en el que estamos escribiendo nuestro código (por ejemplo, porque la recibimos como argumento del método en el que trabajamos o la recibimos como resultado de la invocación de un método cuyo valor de resultado queremos usar), todos quisiéramos evitar este error que tiene el potencial de hacer que nuestras aplicaciones se caigan. Sin embargo, con frecuencia, el problema pasa desapercibido y encuentra la manera de colarse y llegar hasta nuestros críticos sistemas de producción en donde espera el momento propicio para causar un fallo (lo que suele suceder un viernes a eso de las 5 p.m. y justo cuando estamos por dejar la oficina para ir al cine con la familia o ir a tomar unas cervezas con los amigos).

Para complicar las cosas, el lugar en donde se produce el fallo raras veces es el mismo lugar en donde se originó el problema, ya que nuestra problemática referencia podría haber sido inicializada a null en un lugar bastante lejano de aquél en donde hemos intentado utilizarla. Así que es mejor cancelar esos planes de viernes por la noche…

Sir Antony Hoare

Cabe señalar que este concepto potencialmente problemático de las referencias nulas fue introducido originalmente por Antony Hoare, el creador de Algol, allá por el año de 1965. En aquel entonces las consecuencias de su diseño no era evidentes, y el mismo Hoare dice que a él le pareció una buena idea. Sin embargo, más tarde lamentó haberla concebido y la llamó “un error de un billón de dólares“, precisamente refiriéndose a las incontables horas que muchos de nosotros hemos pasado, desde entonces, tratando de arreglar estos problemas de referencias nulas que han sido mal utilizadas.

¿Por qué No Mejorar el Sistema de Tipos?

¿No sería estupendo si el sistema de tipos pudiera ver la diferencia entre una referencia que, en un contexto específico, pudiera ser potencialmente nula de una que no lo es? Esto podría mejorar mucho las cosas en términos de la seguridad de tipos porque el compilador entonces podría exigir que el programador realice alguna verificación para aquellas referencias que pueden ser nulas a la vez que permite el uso directo de aquellas que no lo son.

Entonces hemos identificado aquí una oportunidad de mejora en el sistema de tipos. Esta teórica mejora podría ser particularmente útil en la definición de las interfaces públicas de nuestras APIs porque incrementaría el poder expresivo del lenguaje dándonos una herramienta, aparte de la documentación, para informar a nuestros usuarios sobre si un método dado podría retornar o no un valor.

Antes de que profundicemos más en este tema, es importante aclarar que este es probablemente un ideal que lenguajes de programación modernos perseguirán (ya hablaremos de Ceylon y Kotlin más adelante), pero no es una tarea sencilla tratar de cerrar este agujero en el sistema de tipos de un lenguaje como Java, menos cuando se intenta hacer como una idea de último momento. Así que en los párrafos siguientes se presentan algunos escenarios en los cuales el uso de objetos opcionales puede (cuestionablemente) aliviar esta carga un poco. Aún así, el daño está hecho, y no parece que haya alguna forma sencilla y eficiente de deshacerse de las referencias nulas en el futuro previsible. Así que lo mejor es aprender a lidiar con ellas. Comprender el problema es un primer paso, y en la cuestionable opinión de este escritor, los objetos opcionales son sólo otro mecanismo para lidiar con él, particularmente bajo ciertos escenarios en donde nos gustaría expresar la ausencia de valor, como veremos a continuación.

Búsqueda de Elementos

Existe un conjunto de modismos de programación en lo cuales el uso de referencias nulas es común, pero potencialmente problemático. Uno de estos ejemplos es cuando buscamos un elemento dentro de una colección que finalmente no podemos encontrar. Consideremos ahora la siguiente pieza de código usada para encontrar la primera fruta de cierto nombre dentro de una lista de frutas:

public static Fruit find(String name, List<Fruit> fruits) {
   for(Fruit fruit : fruits) {
      if(fruit.getName().equals(name)) {
         return fruit;
      }
   }
   return null;
}

Como vemos el creador de este código ha hecho el uso de null parte de su diseño para indicar la ausencia de valor: cuando no se encuentre un elemento que satisfaga el criterio de búsqueda este método retorna null (7). Es desafortunado, sin embargo, que no es evidente en la firma de este método que el mismo podría no retornar un valor.

Consideremos ahora un segundo programador que es el consumidor de este método. En el siguiente ejemplo dicho programador intenta utilizar el resultado de una invocación al método anterior:

List<Fruit> fruits = asList(new Fruit("apple"),
                            new Fruit("grape"),
                            new Fruit("orange"));

Fruit found = find("lemon", fruits);
//algún código de por medio y más tarde (posiblemente en otro lugar)...
String name = found.getName(); //uh oh!

Tal simple pieza de código tiene un error que no puede ser detectado por el compilador, ni siquiera por simple observación por parte del programador (quien pudiera no tener acceso al código original del método find). Nuestro programador en este caso ha fallado ingenuamente en reconocer el escenario en el cual el método find de arriba podría retornar un valor nulo para indicar la ausencia de un valor que pueda satisfacer su predicado de búsqueda. Este código está esperando a ser ejecutado para simplemente fallar y ninguna cantidad de documentación va a evitar que este error ocurra, y el compilador ni siquiera se dará por enterado de que existen un problema potencial aquí.

Es de especial atención que la línea en donde la referencia se inicializa a null (5) es diferente de la línea problemática (7). En este caso están lo suficientemente cerca como para corregir el problema fácilmente, pero en la práctica podría no ser tan sencillo.

Para evitar este problema lo que típicamente hacemos es que verificamos si la referencia dada es nula antes de utilizarla. En ciertos casos esta verificación se repite tantas veces para una misma referencia que Martin Fowler (reconocido autor de un libro sobre refactorización de código) sugirió que para este tipo de escenario es posible mitigar la necesidad de realizar estas verificaciones por medio de utilizar lo que él ha llamado un Objeto Nulo. En nuestro ejemplo de arriba, el creador del método find, en vez de retornar null podría haber retornado un objeto NullFruit que no es otra cosa que un tipo de Fruit que está vacío por dentro pero que, a diferencia de una referencia null, puede responder a la misma interfaz pública que Fruit evitando, esta manera, la necesidad de realizar verificaciones de nulidad.

Mínimos y Máximos

Otro lugar en donde este modismo se presenta es cuando se intenta reducir una colección a un valor, por ejemplo a la hora de buscar el valor mínimo o máximo dentro de una colección es un buen ejemplo de esto. Consideremos la siguiente pieza de código en donde se intenta determinar cual es la cadena de caracteres más larga de una colección:

public static String longest(Collection<String> items) {
   if(items.isEmpty()){
      return null;
   }
   Iterator<String> iter = items.iterator();
   String result = iter.next();
   while(iter.hasNext()) {
       String item = iter.next();
       if(item.length() > result.length()){
          result = item;
       }
   }
   return result;
}

En este caso la cuestión radica en el problema de qué se debe retornar si la colección estuviera vacía. En este caso se retorna un valor nulo, una vez más, abriendo con esto la puerta para un potencial problema de utilización de una referencia nula.

Estrategia del Mundo Funcional

Es interesante que en el mundo de la programación funcional, los lenguajes estáticamente tipificados evolucionaron en una dirección muy diferente y representan un contraste valiosísimo ante la deficiente aproximación del paradigma imperativo. En lenguajes como Standard ML o Haskell no existe tal cosa como un valor nulo que cause excepciones cuando se intenta usar. Estos lenguajes, en su lugar, proveen un tipo de datos especial que es capaz de contener valores opcionales y que puede ser utilizando convenientemente para expresar también la potencial ausencia de valor. La siguiente pieza de código muestra la definición del tipo option en SML:

datatype 'a option = NONE | SOME of 'a

Como podemos ver option es un tipo de datos con dos constructores (o lo que se suele conocer como un tipo de unión o suma), uno de ellos no almacena nada (p.ej. NONE) mientras que el otro es capaz de almacenar un valor paramétrico del algún tipo 'a (donde 'a representa el tipo del valor almacenado).

Bajo este modelo, la pieza de código escrita anteriormente en Java para buscar una fruta por nombre puede ser reescrita en SML de la siguiente manera:

fun find(name, fruits) =
   case fruits of
        [] => NONE
      | (Fruit s)::fs => if s = name
                         then SOME (Fruit s)
                         else find(name,fs)

Hay múltiples formas de escribir esto en SML, este ejemplo sólo muestra una forma de hacerlo. Lo importante aquí es que no existe tal cosa como un valor nulo. En su lugar el valor NONE es retornado cuando no se encuentra lo buscado (3) o en el caso contrario un valor SOME f (5) .

Cuando un programador usa esta función find sabe que retorna un tipo option y por lo tanto se ve obligado a verificar la naturaleza del valor obtenido para determinar si es NONE (6) o SOME f (7), más o menos algo así:

let
   val fruits = [Fruit("apple"), Fruit("grape"), Fruit("orange")]
   val found = find("grape", fruits)
in
   case found of
       NONE => print("Nothing found")
     | SOME(Fruit f) => print("Found fruit: " ^ f)
end

El verse en la obligación de verificar la naturaleza del valor retornado hace imposible malinterpretar el resultado.

Tipos Opcionales en Java

Es una alegría que en Java 8 finalmente tendremos una clase llamada Optional que nos permite implementar un modismo similar a este del mundo funcional. Tal como en el caso de SML, este tipo opcional es polimórfico y puede contener un valor o estar vacío. Así las cosas, podemos reescribir nuestro ejemplo anterior de la siguiente manera:

public static Optional<Fruit> find(String name, List<Fruit> fruits) {
   for(Fruit fruit : fruits) {
      if(fruit.getName().equals(name)) {
         return Optional.of(fruit);
      }
   }
   return Optional.empty();
}

Como vemos el método ahora retorna una referencia de tipo Optional (1). Si se llega  encontrar un valor el objeto Optional se construye con el valor encontrado (4), de otra manera se construye un objeto Optional vacío (7).

Ahora nuestro segundo programador y consumidor de esta API escribiría su código de la siguiente manera:

List<Fruit> fruits = asList(new Fruit("apple"),
                            new Fruit("grape"),
                            new Fruit("orange"));

Optional<Fruit> found = find("lemon", fruits);
if(found.isPresent()) {
   Fruit fruit = found.get();
   String name = fruit.getName();
}

Ahora que es evidente en la firma de método find que éste retorna un valor opcional nuestro segundo programador se ve mejor orientado para escribir su código correspondientemente para extraer el valor opcional, siempre que esté presente (6-7).

Vemos que la adopción de este modismo del mundo funcional tiene el potencial de hacer nuestro código más legible, menos propenso al uso de referencias nulas y como resultado más robusto y seguro.

El lector avezado seguramente se apresurará a apuntar que esta solución no resuelve para nada el problema puesto que Optional no es más que una referencia en sí misma que puede ser erróneamente inicializada a null. Más adelante hablaremos en detalle de esto. De momento solo resta decir que se esperaría que los programadores de APIs (como la contiene el método find de arriba) se apeguen a la convención de nunca proveer una referencia nula en donde se espera un Optional mas o menos de la misma manera como es una buena práctica no utilizar una referencia nula en donde se espera una colección o un arreglo; es costumbre, mas bien,  proveer una colección vacía o un arreglo vacío en estos casos.

El punto a resaltar aquí es que existe ahora un mecanismo en la API que podemos utilizar para hacer explícito que en cierto caso debemos lidiar con la posible ausencia de valor y los usuarios de esta API se ven en la obligación de utilizarla correspondientemente. En un artículo sobre el uso de objetos opcionales en el framework de colecciones conocido como Guava se ofrece una explicación magistral:

Además del incremento en la legibilidad que acompaña al hecho de darle a null un nombre, la mayor ventaja del uso de Optional es que es a prueba de idiotas. Simplemente nos fuerza a pensar de forma activa en el caso de ausencia de valor si deseamos que nuestro programa compile del todo, ya que somos forzados a desenvolver el Optional y a atender ese caso.

Otros Métodos Convenientes

Al día de hoy, además de los métodos estáticos of y empty demostrados arriba, la clase Optional contiene los siguientes métodos de conveniencia:

ifPresent() Retorna verdadero si hay un valor presente en el objeto opcional.
get() Retorna el valor contenido dentro del objeto opcional, si existe, de otra manera arroja una excepción  NoSuchElementException.
ifPresent(Consumer<T> consumer) Le proporciona como argumento al consumidor provisto el valor del objeto opcional, si existe. El consumidor se puede implementar mediante una expresión lambda o mediante una referencia de método.
orElse(T other) Retorna el valor del objeto opcional, si existe, de otra manera retorna el otro valor provisto.
orElseGet(Supplier<T> other) Retorna el valor del objeto opcional, si existe, de lo contrario utiliza el suplidor provisto para generar un valor. El suplidor se puede implementar mediante una expresión lambda o una referencia de método.
orElseThrow(Supplier<T> exceptionSupplier) Retorna el valor de objeto opcional, si existe, de otra manera utiliza el suplidor provisto para generar y arrojar una excepción. El suplidor se puede implementar mediante una expresión lambda o un método de referencia.

Cómo Evitar Repetitivas Verificaciones de Presencia

Podemos utilizar algunos de los métodos de conveniencia arriba citados para evitar la necesidad de verificar si el valor del objeto opcional está presente o no. Por ejemplo, una alternativa es utilizar un valor predeterminado en caso de que el objeto opcional este vacío.

Optional<Fruit> found = find("lemon", fruits);
String name = found.orElse(new Fruit("Kiwi")).getName();

En este otro ejemplo, se imprime el nombre de la fruta a la salida del sistema media proveer un consumidor, en este caso implementado mediante una expresión lambda.

Optional<Fruit> found = find("lemon", fruits);
found.ifPresent(f -> { System.out.println(f.getName()); });

En el siguiente ejemplo se utiliza una expresión lambda para implementar un suplidor que puede finalmente proveer un valor predeterminado para el objeto opcional si este estuviera vacío.

Optional<Fruit> found = find("lemon", fruits);
Fruit fruit = found.orElseGet(() -> new Fruit("Lemon"));

Claramente estos métodos de conveniencia simplifican mucho el tener que trabajar con objetos opcionales.

¿Que Hay de Malo con los Objetos Opcionales de Java?

La principal pregunta a la que nos enfrentamos es: ¿se deshacen los objetos opcionales de Java de las referencias nulas? La respuesta es, por supuesto, un enfático ¡no!. Así que los detractores de este enfoque inmediatamente lo cuestionan preguntando: entonces ¿qué de bueno tienen que no pudiéramos hacer ya por otros medios?

A diferencia de los lenguajes funcionales como SML o Haskell que nunca tuvieron el conceptos de referencias nulas, en Java simplemente no podemos deshacernos de ellas como por arte de magia, como si históricamente nunca hubieran existido. Estas continuarán existiendo, y cuestionablemente tienen sus usos apropiados (solo para mencionar un ejemplo, la lógica de tres valores).

Es poco probable que la intención de la clase Optional sea reemplazar el uso de las referencias nulas. Es más probable que su propósito sea ayudar en la creación de APIs más legibles y robustas como se demostró anteriormente. Sin embargo, como se mencionó antes, al final Optional es sólo otra referencia sujeta a las mismas debilidades que todas las demás, así que es claro que Optional no va a salvar el día.

Cómo se deben usar estos tipos opcionales o sí de verdad serán de valor en Java ha sido un tema de agitado debate en la lista de distribución del Proyecto Lambda, que es donde esta clase vio la luz del día por primera vez. De los detractores de esta estrategia escuchamos algunos otros argumentos interesantes que a continuación se enumeran (con toda seguridad no de manera exhaustiva).

  • Existen otras alternativas para lidiar con este problema. Por ejemplo tenemos la JSR-305 sobre la detección de defectos de software que incorpora anotaciones como @Nullable y @NonNull. El entorno integrado de desarrollo Eclipse tiene un conjunto anotaciones propietarias que se pueden usar en el código para realizar análisis estático del código y determinar si una referencia puede ser nula o no.
  • Parte del debate gira en torno al deseo de hacer de los tipos opcionales en Java algo similar a lo que son en el paradigma funcional, lo cual no es simple de implementar pues Java carece de otros importantes mecanismos de programación funcional como la coincidencia de patrones y un sistema de tipos estructural.
  • Una importante queja es la imposibilidad de redefinir el código preexistente para utilizar este modismo. Por ejemplo, List.get(Object) continuará retornando null en los casos en donde no se encuentre el valor buscado. En este caso particular y probablemente en muchos otros en las misma librerías de Java es imposible redefinir el método sin romper la compatibilidad hacia atrás.
  • En esta misma línea de pensamiento, algunos argumentan que la falta de soporte para tipos opcionales al nivel del lenguaje crea un escenario en donde Optional podría ser utilizado de manera inconsistente en las APIs y como consecuencia de esto, creando incompatibilidades, muy similares a las que experimentaremos al combinar código de las  APIs de Java que no usan Optional con otras partes de la API que sí lo hacen.
  • Finalmente, un argumento de bastante peso es que al invocar al método get en un objeto opcional vacío se produce una excepción de NoSuchElementException que es precisamente el mismo problema que tenemos con las referencias nulas, solo que ahora con una excepción diferente. Así que si alguien usa un objeto opcional sin verificar si está vacío podría experimentar el mismo problema de quien usa una referencia nula sin verificar primero si es nula.

Habiendo considerado todo esto, pareciera que los beneficios de Optional se limitan a mejorar la legibilidad y reforzar el uso de los contratos públicos de las interfaces.

Objetos Opcionales en la Nueva API de Streams

Independientemente del debate, los objetos opcionales están aquí para quedarse y ya se están usando en diversos métodos de la nueva API de streams que sera parte del JDK 8, particularmente en métodos como findFirst, findAny, max y min.

Por ejemplo, consideremos la siguiente pieza de código en donde buscamos la última fruta de una colección de frutas en donde buscamos comparando los nombres de las frutas alfabéticamente:

Stream<Fruit> fruits = asList(new Fruit("apple"),
                              new Fruit("grape")).stream();
Optional<Fruit> max = fruits.max(comparing(Fruit::getName));
if(max.isPresent()) {
   String fruitName = max.get().getName(); //grape
}

O este ejemplo en donde extraemos la primera fruta de una colección:

Stream<Fruit> fruits = asList(new Fruit("apple"),
                              new Fruit("grape")).stream();
Optional<Fruit> first = fruits.findFirst();
if(first.isPresent()) {
   String fruitName = first.get().getName(); //apple
}

Tipos Opcionales en el Lenguaje de Programación Ceylon

Recientemente comencé a jugar un poco con el lenguaje de programación Ceylon ya que estoy haciendo una investigación para otro artículo que espero escribir pronto en este blog. Debo admitir que no soy un fanático de Ceylon, de hecho le guardo pocas esperanzas a su popularización. Sin embargo, me pareció muy interesante que en Ceylon este concepto de los valores opcionales se lleva un poco mas lejos y el lenguaje ofrece azúcar sintáctico para este tipo de modismos. En este lenguaje es posible marcar cualquier tipo con un signo de pregunta (?) para convertirlo en un tipo opcional.

Por ejemplo, nuestra función find sería muy similar a la de nuestra versión de Java original, pero esta vez retornando un Fruit? (1), es decir un valor opcional de tipo Fruit. Nótese que aun se utiliza null, pero éste es compatible con la referencia de tipo Fruit? (7).

Fruit? find(String name, List<Fruit> fruits){
   for(Fruit fruit in fruits) {
      if(fruit.name == name) {
         return fruit;
      }
   }
   return null;
}

Y podríamos consumir este método o función con un código como luce a continuación:

List<Fruit> fruits = [Fruit("apple"),Fruit("grape"),Fruit("orange")];
Fruit? fruit = find("lemon", fruits);
print((fruit else Fruit("Kiwi")).name);

Nótese que el uso de la palabra reservada else es muy similar al método orElse de la clase Optional de Java.

Alternativamente podríamos haberlo desarrollado de la siguiente manera:

List<Fruit> fruits = [Fruit("apple"),Fruit("grape"),Fruit("orange")];
Fruit? fruit = find("apple", fruits);
if(exists fruit){
   String fruitName = fruit.name;
   print("The found fruit is: " + fruitName);
} //else...

Nótese que el uso de la palabra reservada exists (3) sirve el mismo propósito que el método isPresent de la clase Optional de Java.

Es digno de mención asimismo que esta sintaxis es muy similar a la de los tipos anulables de C# (nullable types), aunque la semántica es totalmente diferente en el caso Ceylon como hemos podido ver. Asimismo en el lenguaje de programación Kotlin, bajo desarrollo por JetBrains, existe una característica similar a esta relacionada con la seguridad de los valores nulos. Así que podríamos estar frente a una nueva tendencia para lidiar con este problema en lenguajes de programación modernos).

Ahora bien, la gran ventaja de Ceylon y Kotlin sobre Java es que ellos pueden incorporar estos conceptos en el lenguaje y las APIs desde el comienzo liberándose así de las incompatibilidades que plagarán desde ahora las APIs de Java.

Quien sabe, tal vez en futuras entregas de Java se agregue azúcar sintáctico para poder utilizar tipos opcionales como se hace en Ceylon y Kotlin, quizás usando, bajo el capo, la nueva clase Optional de Java 8.

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión /  Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión /  Cambiar )

Conectando a %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

A %d blogueros les gusta esto: