En Java son bien conocidos las NullPointerException
provocadas cuando accedemos a una referencia de un objeto que es null
. A esto Tony Horae lo denominó su error del billón de dólares. En este tutorial veremos cómo podemos combatir este problema en Java.
- 1. Introducción
- 2. Entorno
- 3. Cómo usar la JSR 305 en nuestro proyecto
- 4. Detectando los problemas en tiempo de edición con IntelliJ
- 5. Detectando los problemas en tiempo de compilación con Maven
- 6. Conclusiones
- 7. Sobre el autor
1. Introducción
Java es un lenguaje fuertemente tipado orientado a objetos, donde tendremos variables para referencias a estos objetos. Pero ¿qué pasa si una de estas referencias apunta a null
e intentamos acceder a dicho objeto?
String s = null;
s.toUpperCase()
Estas dos líneas de código son perfectamente válidas y no darán ningún problema al compilarlas, pero cuando las ejecutemos la JVM lanzará la excepción NullPointerException
ya que s
no está referenciando a ningún objeto, y por lo tanto es imposible ejecutar el método toUpperCase()
.
A esta situación Tony Hoare la denominó su error del billón de dólares, y cierto es que lleva desde su invención en 1965 dándonos problemas.
No vamos a entrar en detalle sobre cuáles son los problemas pero básicamente podemos enumerar estos:
- Empeora la legibilidad del código al tener que hacer las comprobaciones para verificar que las referencias no son
null
. - Rompe la filosofía de Java de ocultar a los desarrolladores los punteros.
- Es un agujero en el Sistema de Tipos.
- No tiene significado semántico en particular.
- Es un mal modelo para representar la ausencia de valor para un lenguaje estrictamente tipado y orientado a objetos.
Podéis encontrar más sobre este problema en la presentación de SlideShare “The billion dollar mistake” de Álvaro García Loaisa.
De hecho tanto ha calado este problema que diferentes lenguajes no permiten esta situación, como por ejemplo Kotlin o Swift, obligando al desarrollador a tratar de forma explícita esta situación.
En Java el compilador no nos da ninguna ayuda per se, así que en este tutorial vamos a ver cómo podemos hacer uso de la JSR 305 para evitar, en tiempo de compilación las referencias de objetos a null
.
El código de este tutorial en https://github.com/alejandropg/tutorial-java-jsr305
2. Entorno
El tutorial está escrito usando el siguiente entorno:
- Hardware: Portátil MacBook Pro 15’‘ (2.5 GHz Intel i7, 16GB 1600 Mhz DDR3, 500GB Flash Storage).
- AMD Radeon R9 M370X
- Sistema Operativo: macOS Sierra 10.12.6
- JVM 1.8.0_121 (Oracle Corporation 25.121-b13)
- Maven 3.5.0
3. Cómo usar la JSR 305 en nuestro proyecto
La JSR 305 lo que hace es definir una serie de anotaciones que podemos poner en nuestro código para que otras herramientas las inspeccionen y nos ayuden a detectar posibles problemas.
Para poder usar las anotaciones de la JSR 305 tendemos que añadir a nuestro proyecto Maven la dependencia:
<dependency>
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
<version>3.0.2</version>
</dependency>
Para el caso de las referencias nulas, en concreto las anotaciones que nos interesan son:
@CheckForNull
indica que el valor podría ser nulo en circunstancias no conocidas a priori y por lo tanto debemos comprobar el valor antes de usarlo.@Nonnull
indica que el valor no puede ser nulo.@Nullable
indica que el valor será nulo en circunstancias conocidas a priori. Deberemos leer la documentación para saber cuáles son estas circunstancias.@ParametersAreNonnullByDefault
indica que por defecto todos los parámetros de los métodos se deben interpretar como@Nonnull
@ParametersAreNullableByDefault
indica que por defecto todos los parámetros de los métodos se deben interpretar como@Nullable
Estas anotaciones se pueden usar a nivel de la declaración de un atributo o parámetro o un método. En el caso de usarlas a nivel de método se refieren al tipo de retorno del mismo.
En el caso de @ParametersAreNonnullByDefault
y @ParametersAreNullableByDefault
se puede usar también a nivel de paquete en el fichero package-info.java
. Esto resulta especialmente útil ya que afectará a todos los métodos de todas las clases de ese paquete. Ojo porque esto no es recursivo, es decir no aplica a los sub paquetes, por lo que tendremos que poner este package-info.java
en todos los niveles de la jerarquía de paquetes.
Este package-info.java
tendrá la siguiente forma:
@ParametersAreNonnullByDefault
package com.autentia.tutorial.jsr305;
import javax.annotation.ParametersAreNonnullByDefault;
Estas son las tres líneas que mencionaba en el título de este tutorial y que nos pueden ayudar a solucionar muchos problemas en tiempo de edición y compilación. Es decir, mucho antes de la fase de ejecución o incluso antes de los tests.
4. Detectando los problemas en tiempo de edición con IntelliJ
IntelliJ IDEA es un IDE de desarrollo para Java que detecta automáticamente el uso de estas anotaciones y a través de distintas Inspections va a detectar los posibles problemas de referencias nulas en tiempo de edición del código. Esto es muy interesante ya que detectaremos los errores en el mismo momento en el que escribimos el código.
Así, si abrís con el IntelliJ el código de ejemplo de este tutorial (tenéis la URL en la introducción) veréis las siguientes cosas:
- Service.java
En la siguiente imagen vemos como IntelliJ nos avisa de que el parámetro de entrada, que no debe ser null
tal como está definido en el fichero package-info.java
, está siendo llamado desde otro sitio con una referencia nula como argumento.
Ahora nos avisa que, si parameter
no puede ser null
esa comprobación que hace el if
es redundante.
También se da cuenta de que estamos intentando devolver un null
como valor de retorno del método.
En la siguiente imagen se aprecian dos cosas, la primera que nos avisa de que s
puede ser null
y por lo tanto provocar un NullPointerException
, y la segunda (aunque no se ve muy bien porque está medio tapado por el mensaje) es que estamos haciendo un return null
y no se está quejando tal como lo hacía en el caso anterior. Esto es porque hemos anotado el método con @Nullable
sobreescribiendo así el valor por defecto.
- Client.java
Detecta como intentamos pasar un argumento con una referencia nula a un método que no lo permite.
Nótese como el primer uso de trim()
es válido, mientras que en segundo IntelliJ nos avisa que no es correcto. Esto se debe a que el método returnAlwaysNull()
está anotado con @Nullable
y por lo tanto estamos obligados a comprobar si el valor es nulo si no queremos tener problemas en tiempo de ejecución.
5. Detectando los problemas en tiempo de compilación con Maven
El poder detectar los posibles errores mientras editamos el código es muy cómodo, pero es bastante débil en el sentido que depende de qué IDE usa cada miembro del equipo y de cómo lo tiene configurado.
Para evitar estos problemas “multi entorno”, lo que vamos a hacer es configurar nuestro proyecto de Maven para se hagan las comprobaciones de nulos durante la compilación. Para ello nos vamos a servir del Procesamiento de Anotaciones que es una característica que está disponible desde Java 5 aunque realmente el API está desde Java 6 (diciembre 2006). Esta característica nos va a permitir “enganchar” a la compilación un procesador de anotaciones que se encargará de hacer las comprobaciones pertinentes.
Para ello en nuestro pom.xml
basta con añadir:
...
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.7.0</version>
<configuration>
<annotationProcessors>
<annotationProcessor>org.checkerframework.checker.nullness.NullnessChecker</annotationProcessor>
</annotationProcessors>
</configuration>
</plugin>
</plugins>
</build>
...
<dependencies>
...
<dependency>
<groupId>org.checkerframework</groupId>
<artifactId>checker</artifactId>
<version>2.2.0</version>
<scope>provided</scope>
</dependency>
...
</dependencies>
...
En el ejemplo estamos usando The Checker Framework que se trata de un proyecto Free Software, con licencia GPL2, que nos proporciona distintas utilidades o “procesadores” para la prevención de bugs en tiempo de compilación. En el ejemplo estamos usando el NullnessChecker
pero dispone de otros tantos para prevenir otras situaciones.
Podemos destacar como hemos puesto la dependencia como provided
ya que realmente sólo la necesitamos durante el proceso de compilación y no queremos arrastrarla cuando preparemos el “distribuible” del proyecto.
Ahora al compilar en la línea de comandos detectaremos todos los errores:
$ mvn clean install
...
[INFO] -------------------------------------------------------------
[ERROR] COMPILATION ERROR :
[INFO] -------------------------------------------------------------
[ERROR] /Users/alex/src/sandbox/tutorial-java-jsr305/src/main/java/com/autentia/tutorial/jsr305/Client.java:[8,33] [argument.type.incompatible] incompatible types in argument.
found : null
required: @Initialized @NonNull String
[ERROR] /Users/alex/src/sandbox/tutorial-java-jsr305/src/main/java/com/autentia/tutorial/jsr305/Client.java:[14,29] [dereference.of.nullable] dereference of possibly-null reference s2
[ERROR] /Users/alex/src/sandbox/tutorial-java-jsr305/src/main/java/com/autentia/tutorial/jsr305/Service.java:[15,16] [return.type.incompatible] incompatible types in return.
found : null
required: @Initialized @NonNull String
[ERROR] /Users/alex/src/sandbox/tutorial-java-jsr305/src/main/java/com/autentia/tutorial/jsr305/Service.java:[21,9] [dereference.of.nullable] dereference of possibly-null reference s
[INFO] 4 errors
[INFO] -------------------------------------------------------------
...
6. Conclusiones
En determinados casos el uso de nulos en nuestro código puede ser beneficioso, pero por lo general debemos evitarlos.
Con este tutorial hemos visto cómo podemos configurar nuestro proyecto para añadir restricciones en tiempo de edición y compilación al uso de referencias nulas, de forma que conseguimos prevenir de forma temprana errores que sólo veríamos en tiempo de ejecución.
Además el método visto aquí es flexible en el sentido de que, en los casos que nos interese, podemos usar las anotaciones para especificar donde sí queremos usar nulos, de forma que no perdemos potencia y sí ganamos control.
Desde luego mi recomendación sería que, independientemente de si usáis este tutorial o no, huyáis de los nulos como de la peste.
7. Sobre el autor
Alejandro Pérez García (@alejandropgarci)
Ingeniero en Informática (especialidad de Ingeniería del Software) y Certified ScrumMaster
Socio fundador de Autentia Real Business Solutions S.L. – “Soporte a Desarrollo”
Socio fundador de ThE Audience Megaphone System, S.L. – TEAMS – “Todo el potencial de tus grupos de influencia a tu alcance”
Muchas gracias por esta píldora de conocimiento.