Manejo efectivo de NullPointerException de Java

No se necesita mucha experiencia en desarrollo de Java para aprender de primera mano de qué se trata NullPointerException. De hecho, una persona ha destacado que lidiar con esto es el error número uno que cometen los desarrolladores de Java. Escribí anteriormente en un blog sobre el uso de String.value (Object) para reducir las NullPointerExceptions no deseadas. Hay varias otras técnicas simples que uno puede usar para reducir o eliminar las ocurrencias de este tipo común de RuntimeException que ha estado con nosotros desde JDK 1.0. Esta publicación de blog recopila y resume algunas de las técnicas más populares.

Compruebe que cada objeto no sea nulo antes de usarlo

La forma más segura de evitar una NullPointerException es verificar todas las referencias de objeto para asegurarse de que no sean nulas antes de acceder a uno de los campos o métodos del objeto. Como indica el siguiente ejemplo, esta es una técnica muy sencilla.

final String causeStr = "adding String to Deque that is set to null."; final String elementStr = "Fudd"; Deque deque = null; try { deque.push(elementStr); log("Successful at " + causeStr, System.out); } catch (NullPointerException nullPointer) { log(causeStr, nullPointer, System.out); } try { if (deque == null) { deque = new LinkedList(); } deque.push(elementStr); log( "Successful at " + causeStr + " (by checking first for null and instantiating Deque implementation)", System.out); } catch (NullPointerException nullPointer) { log(causeStr, nullPointer, System.out); } 

En el código anterior, el Deque usado se inicializa intencionalmente a nulo para facilitar el ejemplo. El código del primer trybloque no comprueba si es nulo antes de intentar acceder a un método Deque. El código del segundo trybloque comprueba si es nulo e instancia una implementación de Deque(LinkedList) si es nulo. La salida de ambos ejemplos se ve así:

ERROR: NullPointerException encountered while trying to adding String to Deque that is set to null. java.lang.NullPointerException INFO: Successful at adding String to Deque that is set to null. (by checking first for null and instantiating Deque implementation) 

El mensaje que sigue a ERROR en la salida anterior indica que NullPointerExceptionse lanza un cuando se intenta una llamada a un método en el nulo Deque. El mensaje que sigue a INFO en el resultado anterior indica que al verificar Dequeprimero si hay nulo y luego instanciar una nueva implementación cuando es nulo, la excepción se evitó por completo.

Este enfoque se usa a menudo y, como se muestra arriba, puede ser muy útil para evitar NullPointerExceptioninstancias no deseadas (inesperadas) . Sin embargo, no está exento de costos. La verificación de nulos antes de usar cada objeto puede sobrecargar el código, puede ser tedioso de escribir y abre más espacio para problemas con el desarrollo y mantenimiento del código adicional. Por esta razón, se ha hablado de introducir el soporte del lenguaje Java para la detección de nulos incorporada, la adición automática de estas comprobaciones de nulos después de la codificación inicial, los tipos seguros para nulos, el uso de la programación orientada a aspectos (AOP) para agregar la verificación de nulos. al código de bytes y otras herramientas de detección de nulos.

Groovy ya proporciona un mecanismo conveniente para tratar con referencias de objetos que son potencialmente nulas. El operador de navegación segura de Groovy ( ?.) devuelve un valor nulo en lugar de lanzar un NullPointerExceptioncuando se accede a una referencia de objeto nulo.

Debido a que verificar nulo para cada referencia de objeto puede ser tedioso y sobrecarga el código, muchos desarrolladores eligen seleccionar juiciosamente qué objetos buscar nulos. Por lo general, esto conduce a la verificación de nulos en todos los objetos de orígenes potencialmente desconocidos. La idea aquí es que los objetos se pueden verificar en las interfaces expuestas y luego se supone que son seguros después de la verificación inicial.

Ésta es una situación en la que el operador ternario puede resultar particularmente útil. En vez de

// retrieved a BigDecimal called someObject String returnString; if (someObject != null) { returnString = someObject.toEngineeringString(); } else { returnString = ""; } 

el operador ternario admite esta sintaxis más concisa

// retrieved a BigDecimal called someObject final String returnString = (someObject != null) ? someObject.toEngineeringString() : ""; } 

Comprobar argumentos de método para nulo

La técnica que acabamos de comentar se puede utilizar en todos los objetos. Como se indica en la descripción de esa técnica, muchos desarrolladores optan por comprobar si los objetos son nulos cuando provienen de fuentes "no confiables". A menudo, esto significa probar primero el valor nulo en métodos expuestos a llamadores externos. Por ejemplo, en una clase en particular, el desarrollador puede optar por buscar nulos en todos los objetos pasados ​​a publicmétodos, pero no verificar nulos en los privatemétodos.

El siguiente código demuestra esta comprobación de nulos en la entrada del método. Incluye un método único como método demostrativo que da la vuelta y llama a dos métodos, pasando a cada método un único argumento nulo. Uno de los métodos que recibe un argumento nulo comprueba primero ese argumento en busca de nulo, pero el otro simplemente asume que el parámetro pasado no es nulo.

 /** * Append predefined text String to the provided StringBuilder. * * @param builder The StringBuilder that will have text appended to it; should * be non-null. * @throws IllegalArgumentException Thrown if the provided StringBuilder is * null. */ private void appendPredefinedTextToProvidedBuilderCheckForNull( final StringBuilder builder) { if (builder == null) { throw new IllegalArgumentException( "The provided StringBuilder was null; non-null value must be provided."); } builder.append("Thanks for supplying a StringBuilder."); } /** * Append predefined text String to the provided StringBuilder. * * @param builder The StringBuilder that will have text appended to it; should * be non-null. */ private void appendPredefinedTextToProvidedBuilderNoCheckForNull( final StringBuilder builder) { builder.append("Thanks for supplying a StringBuilder."); } /** * Demonstrate effect of checking parameters for null before trying to use * passed-in parameters that are potentially null. */ public void demonstrateCheckingArgumentsForNull() { final String causeStr = "provide null to method as argument."; logHeader("DEMONSTRATING CHECKING METHOD PARAMETERS FOR NULL", System.out); try { appendPredefinedTextToProvidedBuilderNoCheckForNull(null); } catch (NullPointerException nullPointer) { log(causeStr, nullPointer, System.out); } try { appendPredefinedTextToProvidedBuilderCheckForNull(null); } catch (IllegalArgumentException illegalArgument) { log(causeStr, illegalArgument, System.out); } } 

Cuando se ejecuta el código anterior, la salida aparece como se muestra a continuación.

ERROR: NullPointerException encountered while trying to provide null to method as argument. java.lang.NullPointerException ERROR: IllegalArgumentException encountered while trying to provide null to method as argument. java.lang.IllegalArgumentException: The provided StringBuilder was null; non-null value must be provided. 

En ambos casos, se registró un mensaje de error. Sin embargo, el caso en el que se verificó un nulo arrojó una IllegalArgumentException anunciada que incluía información de contexto adicional sobre cuándo se encontró el nulo. Alternativamente, este parámetro nulo podría haberse manejado de varias formas. Para el caso en el que no se manejó un parámetro nulo, no había opciones sobre cómo manejarlo. Muchas personas prefieren lanzar un NullPolinterExceptioncon la información de contexto adicional cuando se descubre explícitamente un nulo (consulte el artículo # 60 en la segunda edición de Effective Java o el artículo # 42 en la primera edición), pero tengo una ligera preferencia por IllegalArgumentExceptioncuando es explícitamente un argumento del método que es nulo porque creo que la misma excepción agrega detalles de contexto y es fácil incluir "nulo" en el asunto.

La técnica de comprobar los argumentos del método en busca de nulos es realmente un subconjunto de la técnica más general de comprobar todos los objetos en busca de valores nulos. Sin embargo, como se describió anteriormente, los argumentos de los métodos expuestos públicamente son a menudo los menos confiables en una aplicación y, por lo tanto, verificarlos puede ser más importante que verificar si el objeto promedio es nulo.

La verificación de los parámetros del método en busca de valores nulos también es un subconjunto de la práctica más general de verificar la validez general de los parámetros del método, como se explica en el artículo 38 de la segunda edición de Effective Java (artículo 23 en la primera edición).

Considere primitivas en lugar de objetos

No creo que sea una buena idea seleccionar un tipo de datos primitivo (como int) sobre su tipo de referencia de objeto correspondiente (como Integer) simplemente para evitar la posibilidad de a NullPointerException, pero no se puede negar que una de las ventajas de tipos primitivos es que no conducen a NullPointerExceptions. Sin embargo, es necesario comprobar la validez de las primitivas (un mes no puede ser un número entero negativo), por lo que este beneficio puede ser pequeño. Por otro lado, las primitivas no se pueden usar en las colecciones de Java y hay ocasiones en las que uno desea tener la capacidad de establecer un valor en nulo.

Lo más importante es tener mucho cuidado con la combinación de primitivas, tipos de referencia y autoboxing. Hay una advertencia en Effective Java (segunda edición, elemento n. ° 49) con respecto a los peligros, incluido el lanzamiento NullPointerException, relacionados con la mezcla descuidada de tipos primitivos y de referencia.

Considere cuidadosamente las llamadas a métodos encadenados

A NullPointerExceptionpuede ser muy fácil de encontrar porque un número de línea indicará dónde ocurrió. Por ejemplo, un seguimiento de pila podría verse como el que se muestra a continuación:

java.lang.NullPointerException at dustin.examples.AvoidingNullPointerExamples.demonstrateNullPointerExceptionStackTrace(AvoidingNullPointerExamples.java:222) at dustin.examples.AvoidingNullPointerExamples.main(AvoidingNullPointerExamples.java:247) 

El seguimiento de la pila hace que sea obvio que NullPointerExceptionse lanzó como resultado del código ejecutado en la línea 222 de AvoidingNullPointerExamples.java. Incluso con el número de línea proporcionado, aún puede ser difícil delimitar qué objeto es nulo si hay varios objetos con métodos o campos a los que se accede en la misma línea.

Por ejemplo, una declaración como someObject.getObjectA().getObjectB().getObjectC().toString();tiene cuatro posibles llamadas que podrían haber arrojado el NullPointerExceptionatributo a la misma línea de código. El uso de un depurador puede ayudar con esto, pero puede haber situaciones en las que sea preferible simplemente dividir el código anterior para que cada llamada se realice en una línea separada. Esto permite que el número de línea contenido en un seguimiento de pila indique fácilmente qué llamada exacta fue el problema. Además, facilita la verificación explícita de cada objeto en busca de nulos. Sin embargo, en el lado negativo, dividir el código aumenta la línea de recuento de código (¡para algunos eso es positivo!) Y no siempre es deseable, especialmente si uno está seguro de que ninguno de los métodos en cuestión será nulo.

Hacer NullPointerExceptions más informativas

In the above recommendation, the warning was to consider carefully use of method call chaining primarily because it made having the line number in the stack trace for a NullPointerException less helpful than it otherwise might be. However, the line number is only shown in a stack trace when the code was compiled with the debug flag turned on. If it was compiled without debug, the stack trace looks like that shown next:

java.lang.NullPointerException at dustin.examples.AvoidingNullPointerExamples.demonstrateNullPointerExceptionStackTrace(Unknown Source) at dustin.examples.AvoidingNullPointerExamples.main(Unknown Source) 

Como demuestra el resultado anterior, hay un nombre de método, pero no un número de línea para NullPointerException. Esto hace que sea más difícil identificar inmediatamente qué en el código condujo a la excepción. Una forma de abordar esto es proporcionar información de contexto en cualquier lanzamiento NullPointerException. Esta idea se demostró anteriormente cuando NullPointerExceptionse capturó y se volvió a lanzar con información de contexto adicional como IllegalArgumentException. Sin embargo, incluso si la excepción simplemente se vuelve a lanzar como otra NullPointerExceptioncon información de contexto, sigue siendo útil. La información de contexto ayuda a la persona que depura el código a identificar más rápidamente la verdadera causa del problema.

El siguiente ejemplo demuestra este principio.

final Calendar nullCalendar = null; try { final Date date = nullCalendar.getTime(); } catch (NullPointerException nullPointer) { log("NullPointerException with useful data", nullPointer, System.out); } try { if (nullCalendar == null) { throw new NullPointerException("Could not extract Date from provided Calendar"); } final Date date = nullCalendar.getTime(); } catch (NullPointerException nullPointer) { log("NullPointerException with useful data", nullPointer, System.out); } 

El resultado de ejecutar el código anterior se ve como sigue.

ERROR: NullPointerException encountered while trying to NullPointerException with useful data java.lang.NullPointerException ERROR: NullPointerException encountered while trying to NullPointerException with useful data java.lang.NullPointerException: Could not extract Date from provided Calendar