Hoy voy a hablar sobre la nulabilidad en Kotlin.

Un poco de historia:

Como sabemos, tradicionalmente en los lenguajes de programación los tipos de valor no son nullables, mientras que los tipos de referencia sí lo son. Los tipos por referencia, generalmente objetos, tienen su contenido en una zona de memoria y cuando tenemos ese objeto en la pila o lo pasamos a otra función, con lo que estamos trabajando es con una referencia, con un puntero.

Al final un puntero acaba siendo un número que hace referencia a una posición en la memoria.
Y la referencia nula (o puntero a la posición 0) originalmente no tenía ningún significado en especial. Al principio se aprovechaba toda la memoria que era poco y no había memoria virtual ni traducciones de direcciones ni nada de eso.

Al final generalmente cuando algo había ido mal, acabábamos con punteros que hacían referencia a posiciones inválidas o que no queríamos, que muchas veces eran valores bajos que hacían referencia a partes bajas de la memoria. Por ejemplo si hacíamos un “malloc” que fallaba, esto devolvía NULL, un puntero a la posición 0, y si luego accedíamos en un campo, igual estábamos accediendo a la posición de memoria 96 por ejemplo. Lo cual era bastante peliagudo. Pero con arquitecturas sistemas operativos más modernos, esto empezó a suponer violaciones de acceso y no reinicios del ordenador, bucles infinitos o crashes legendarios. La aplicación petaba y la podíamos volver a arrancar.

Nulabilidad en Java:

Una parte bastante importante de las excepciones en runtime de Java se produce por null pointer exceptions. Siempre dependiendo del tipo de proyecto, por supuesto. En un IDE o un compilador por ejemplo la cantidad de nulls que pueden haber es abrumadora. El usuario va haciendo programas incompletos y las estructuras internas, los ASTs que maneja el IDE pueden tener partes nulas en muchísimos lugares. Y gran parte del código acaba siendo para la gestión de nulos.

Los tíos de JetBrains, para no volverse gilipollas, metieron dos anotaciones soportadas en sus IDEs: @Nullable y @NotNull.

Esto permite anotar valores de retorno, argumentos y campos como que aceptan nulos o no aceptan nulos. Y con ello logran hacer un análisis estático que nos ayuda a llegar a runtime con menos de estos errores.

Todo esto son warnings y no son errores de compilación, pero intelliJ genera código que hace aserciones en runtime y que lanza excepciones cuanto antes si no se cumple alguno de los contratos que hemos anotado.

En relación a esto los de JetBrains desarrollaron KAnnotator, un proyecto que permite el análisis de librerías Java a nivel de bytecode para determinar si una función acepta o si devuelve nulos automágicamente (basándose en la comprobación y el seteo de nulos dentro de ese código).

Nulabilidad en Kotlin

En Kotlin, la gestión de nulabilidad está a otro nivel. Siendo una parte intrínseca del lenguaje. Como ocurre en otros lenguajes modernos como Swift.

Básicamente ningún tipo, bien sea un tipo referencia o valor, se puede nulificar a nivel de lenguaje por defecto. Y esto está forzado no en tiempo de edición, sino en tiempo de compilación como errores.

Llamar a métodos o acceder a campos con tipos nulables

Intentar acceder de forma normal a un miembro de un objeto de un tipo nulable es un error y no se permite.

En estos casos tenemos varias opciones:
Utilizar el operador de acceso con propagación de nulos **?.** como sugiere el IDE y que tiene un quick-fix.
this.getName()?.endsWith("." + ext)

El tipo de la expresión es Boolean?, es decir que si endsWith devuelve un Boolean, al utilizar el operador ?. hace que pueda ser nulo también. Devolverá true o false si el nombre terminar por la extensión puesta o nulo si el nombre del archivo es nulo. Pero en ningún caso lanzará una NullPointerException.

También podemos utilizar el shortcut de casteo a tipo no nulo !! que es un operador unario colocado a la derecha.

this.getName()!!.endsWith("." + ext)

Ahí estamos aseverando que this.getName() devuelve una cadena y no va a ser null esa cadena, y sí producirá una excepción en el caso de que no lo sea. Y el resultado de la expresión es un Boolean no nulable.

La otra opción es hacer uso del smart cast. Cuando hacemos una comprobación de un valor inmutable (val) sobre su nulidad, bien sea en una cláusula de guarda o en una rama de ifs o de whens, el compilador hace un smart cast de ese tipo que era nulable a no nulable. Por ejemplo:

val name = this.getName()
if (name == null) return false
name.endsWith("." + ext)

Ahí se ha producido un smart cast después de la cláusula de guarda indicando que name es con total certeza no nulo. Y tanto el compilador como el IDE lo saben y después del id, name está casteado a String. Eso sí, antes de la cláusula de guarda, el tipo name era String?.