NullPointerException
, no creo equivocarme al afirmar que difícilmente encontraremos un programador de Java que no haya visto esa excepción en su vida. Esta excepción nos indica que hemos intentado utilizar un null
donde un objeto era requerido, esto incluye intentar:
- Ejecutar un método en un
null
- Acceder o modificar un campo de un
null
- Obtener la longitud de un
null
como si fuera unarray
- Acceder o modificar las casillas de un
null
como si fuera unarray
Throw
un null como si fuera un valorThrowable
Algunas practicas hacen que su frecuencia sea mayor en algunos proyectos más que en otros, pero me atrevería a decir que el solo hecho de no retornar null
mejorará la robustez del código y disminuirá la presencia de los NullPointerException
en los logs.
Principalmente, los null
se suelen retornar en 2 casos: (1) denotar que no se encontraron valores para llenar una colección, (2) señalar que un valor no se encontró o no estaba presente, incluso aunque no haya habido ningún error.
Estos 2 casos se pueden afrontar de mejor forma evitando retornar null
y por lo tanto causando que los clientes de nuestro código no tengan que adivinar si deben verificar que el valor retornado sea null
o no antes de hacer algo con él.
Colección vacía
Cuando un método retorna una lista (o cualquier otra colección) de elementos, puede ser común retornar null
para señalar que ningún elemento se pudo encontrar para la lista solicitada. Por ejemplo, pudiéramos implementar un servicio encargado de gestionar los usuarios de una plataforma de la siguiente forma (algunos métodos y clases han sido omitidos para simplificar):
public class UserService {
public List<User> getUsers() {
User[] usersFromDb = getUsersFromDatabase();
if (usersFromDb == null) {
// No users found in database
return null;
}
else {
return Arrays.asList(usersFromDb);
}
}
}
UserServer service = new UserService();
List<Users> users = service.getUsers();
if (users != null) {
for (User user: users) {
System.out.println("User found: " + user.getName());
}
}
Como hemos elegido retornar null
en caso de que no hayan usuarios, estamos forzando a nuestros clientes a tener en cuenta este caso antes de intentar iterar sobre la lista de usuarios. Si en lugar de ello, retornaramos una lista vacia para denotar que ningún usuario fue encontrado, el cliente pudiera remover la verificación del null
y simplemente iterar sobre la lista de usuarios. Si no hay ningún usuario, la iteración se saltará implícitamente sin tener que verificar manualmente dicho caso; en esencia, iterar sobre la lista de usuarios funciona como queremos en ambos casos (con usuarios o vacía) sin tener que revisar manualmente ambos casos.
public class UserService {
public List<User> getUsers() {
User[] usersFromDb = getUsersFromDatabase();
if (usersFromDb == null) {
// No users found in database
return Collections.emptyList();
}
else {
return Arrays.asList(usersFromDb);
}
}
}
UserServer service = new UserService();
List<Users> users = service.getUsers();
for (User user: users) {
System.out.println("User found: " + user.getName());
}
Todo se resume al consejo de Effective Java que sugiere que debemos de retornar una colección vacía para denotar que no hay elementos, no null
.
Optional
En ocaciones, retornamos null
para informar al cliente que un valor opcional no se encuentra presente, no porque sea un error, sino porque su presencia o ausencia son estados validos. Por ejemplo, obtener un parametro de un URL. En algunos casos, el parametro puede estar presente, pero en otros no. La ausencia del parametro no necesariamente representa un error, sino que el usuario no deseaba la funcionalidad que provee dicho parametro. Esto se puede lograr retornando null
si el parámetro no está presente o el valor del parámetro si llega a estar presente (algunos métodos han sido omitidos):
public class UserListUrl {
private final String url;
public UserListUrl(String url) {
this.url = url;
}
public String getSortingValue() {
if (urlContainsSortParameter(url)) {
return extractSortParameter(url);
}
else {
return null;
}
}
}
UserService userService = new UserService();
UserListUrl url = new UserListUrl("http://localhost/api/v2/users");
String sortingParam = url.getSortingValue();
if (sortingParam != null) {
UserSorter sorter = UserSorter.fromParameter(sortingParam);
return userService.getUsers(sorter);
}
else {
return userService.getUsers();
}
En este ejemplo, cuando no se provee un parámetro para ordenar se retorna null
y el cliente debe de manejar este caso, sin dejar claro en la firma del método getSortingValue
que el valor del parámetro para ordenar es opcional. Para saber que el valor de este parametro es opcional y que nos retornará null
si no está presente, debemos ir a la fuente del código o a la documentación (de ser posible).
Sin embargo, podemos hacer explicita la naturaleza opcional de este parámetro, cambiando la firma de nuestro método getSortingValue
para que retorne un objecto del tipo Optional
. Como se puede observar, el cliente sigue teniendo que manejar el caso en el que el parámetro no está presente, pero ahora la necesidad del mismo está explicita. Incluso, la clase Optional
provee de mecanismos para manejar la ausencia de un parámetro mas allá de simplemente revisar si es null
. Por ejemplo, podemos simplemente verificar la presencia del parametro con el metodo isPresent
que provee Optional
:
public class UserListUrl {
private final String url;
public UserListUrl(String url) {
this.url = url;
}
public Optional<String> getSortingValue() {
if (urlContainsSortParameter(url)) {
return Optional.of(extractSortParameter(url));
}
else {
return Optional.empty();
}
}
}
UserService userService = new UserService();
UserListUrl url = new UserListUrl("http://localhost/api/v2/users");
Optional<String> sortingParam = url.getSortingValue();
if (sortingParam.isPresent()) {
UserSorter sorter = UserSorter.fromParameter(sortingParam.get());
return userService.getUsers(sorter);
}
else {
return userService.getUsers();
}
Si, es casi idéntico al código donde revisamos la nulidad del objeto, pero en este caso hemos hecho explicita la naturaleza opcional del parámetro y quien use este método sabrá que puede esperar inmediatamente al ver la firma del método. Si no estuviésemos interesados en retornar la lista de usuarios en base al parámetro opcional de la URL, sino en consumir el parámetro de alguna forma, pudiéramos utilizar el método ifPresentOrElse
de la siguiente forma:
sortingParam.ifPresentOrElse(
param -> System.out.println("Parameter is :" + param),
() -> System.out.println("No parameter supplied.")
);
Esto reduce toda la ceremonia requerida para verificar la nulidad de una variable. Incluso, si no nos importa el caso en el que no está presente el valor, podemos hacerlo con ifPresent
de la siguiente forma:
sortingParam.ifPresent(param -> System.out.println("Parameter is :" + param));
En cualquier caso, utilizando un objeto Optional
, en lugar de retornar un null
, le informamos explicitamente a nuestro cliente que el valor de retorno puede o no estar presente e incluso le ofrecemos múltiples formas de manejar ese valor. Por lo que como norma general, si el valor de retorno de una función es opcional, asegura que tus clientes manejen la presencia o ausencia del valor retornando un objeto del tipo Optional
.
Ejemplos extraídos de 3 Things Every Java Developer Should Stop Doing