Java Language
Errores de Java - Nulls y NullPointerException
Buscar..
Observaciones
El valor null
es el valor predeterminado para un valor no inicializado de un campo cuyo tipo es un tipo de referencia.
NullPointerException
(o NPE) es la excepción que se produce cuando intenta realizar una operación inapropiada en la referencia de objeto null
. Tales operaciones incluyen:
- llamando a un método de instancia en un objeto de destino
null
, - accediendo a un campo de un objeto objetivo
null
, - intentando indexar un objeto de matriz
null
o acceder a su longitud, - usando una referencia de objeto
null
como mutex en un bloquesynchronized
, - echando una referencia de objeto
null
, - Unboxing una referencia de objeto
null
, y - lanzando una referencia de objeto
null
.
Las causas de raíz más comunes para NPEs:
- olvidando inicializar un campo con un tipo de referencia,
- olvidarse de inicializar elementos de una matriz de un tipo de referencia, o
- no probar los resultados de ciertos métodos de API que se especifican como devueltos
null
en ciertas circunstancias.
Ejemplos de métodos comúnmente usados que devuelven null
incluyen:
- El método de
get(key)
en la API deMap
devolverá unnull
si lo llama con una clave que no tiene una asignación. - Los
getResource(path)
ygetResourceAsStream(path)
en las API deClassLoader
yClass
devolverán elnull
si no se puede encontrar el recurso. - El método
get()
en la API deReference
devolverá unnull
si el recolector de basura ha borrado la referencia. - Varios métodos
getXxxx
en las API del servlet de Java EE devolverán unnull
si intenta recuperar un parámetro de solicitud, una sesión o un atributo de sesión inexistentes, etc.
Existen estrategias para evitar las NPE no deseadas, como probar explícitamente la existencia de valores null
o el uso de la "Notación Yoda", pero estas estrategias a menudo tienen el resultado indeseable de ocultar problemas en su código que realmente deberían solucionarse.
Trampa: el uso innecesario de envolturas primitivas puede llevar a NullPointerExceptions
A veces, los programadores que son nuevos Java usarán tipos primitivos y envoltorios de manera intercambiable. Esto puede llevar a problemas. Considera este ejemplo:
public class MyRecord {
public int a, b;
public Integer c, d;
}
...
MyRecord record = new MyRecord();
record.a = 1; // OK
record.b = record.b + 1; // OK
record.c = 1; // OK
record.d = record.d + 1; // throws a NullPointerException
Nuestra clase 1 de MyRecord
basa en la inicialización predeterminada para inicializar los valores en sus campos. Por lo tanto, cuando new
un registro, los a
y b
campos se ponen a cero, y los c
y d
campos se establecerán en null
.
Cuando intentamos usar los campos inicializados predeterminados, vemos que los campos int
funcionan todo el tiempo, pero los campos de Integer
funcionan en algunos casos y no en otros. Específicamente, en el caso de que falle (con d
), lo que sucede es que la expresión del lado derecho intenta desempaquetar una referencia null
, y eso es lo que hace que se lance la excepción NullPointerException
.
Hay un par de maneras de ver esto:
Si los campos
c
yd
deben ser envoltorios primitivos, entonces no debemos confiar en la inicialización predeterminada, o deberíamos estar comprobando que nonull
. Para anterior es el enfoque correcto a menos que haya un significado definido para los campos en estadonull
.Si los campos no necesitan ser envoltorios primitivos, entonces es un error hacerlos envoltorios primitivos. Además de este problema, los envoltorios primitivos tienen gastos generales adicionales en relación con los tipos primitivos.
La lección aquí es no usar tipos de envoltorios primitivos a menos que realmente lo necesite.
1 - Esta clase no es un ejemplo de buenas prácticas de codificación. Por ejemplo, una clase bien diseñada no tendría campos públicos. Sin embargo, ese no es el punto de este ejemplo.
Pitfall - Usar nulo para representar una matriz o colección vacía
Algunos programadores piensan que es una buena idea ahorrar espacio utilizando un null
para representar una matriz o colección vacía. Si bien es cierto que puede ahorrar una pequeña cantidad de espacio, la otra cara es que hace que su código sea más complicado y más frágil. Compara estas dos versiones de un método para sumar una matriz:
La primera versión es cómo normalmente codificarías el método:
/**
* Sum the values in an array of integers.
* @arg values the array to be summed
* @return the sum
**/
public int sum(int[] values) {
int sum = 0;
for (int value : values) {
sum += value;
}
return sum;
}
La segunda versión es cómo debe codificar el método si tiene la costumbre de usar null
para representar una matriz vacía.
/**
* Sum the values in an array of integers.
* @arg values the array to be summed, or null.
* @return the sum, or zero if the array is null.
**/
public int sum(int[] values) {
int sum = 0;
if (values != null) {
for (int value : values) {
sum += value;
}
}
return sum;
}
Como puedes ver, el código es un poco más complicado. Esto es directamente atribuible a la decisión de usar null
de esta manera.
Ahora considere si esta matriz que podría ser null
se usa en muchos lugares. En cada lugar donde lo use, debe considerar si necesita realizar una prueba de null
. Si pierde una prueba null
que debe estar allí, se arriesga a una NullPointerException
. Por lo tanto, la estrategia de utilizar null
de esta manera hace que su aplicación sea más frágil; Es decir, más vulnerable a las consecuencias de los errores de programación.
La lección aquí es usar matrices vacías y listas vacías cuando eso es lo que quieres decir.
int[] values = new int[0]; // always empty
List<Integer> list = new ArrayList(); // initially empty
List<Integer> list = Collections.emptyList(); // always empty
La sobrecarga de espacio es pequeña, y hay otras formas de minimizarlo si esto es algo que vale la pena hacer.
Pitfall - "Haciendo buenos" nulos inesperados
En StackOverflow, a menudo vemos código como este en Respuestas:
public String joinStrings(String a, String b) {
if (a == null) {
a = "";
}
if (b == null) {
b = "";
}
return a + ": " + b;
}
A menudo, esto se acompaña con una afirmación que es "la mejor práctica" para realizar una prueba null
como esta para evitar NullPointerException
.
¿Es la mejor práctica? En resumen: No.
Hay algunas suposiciones subyacentes que deben ser cuestionadas antes de que podamos decir si es una buena idea hacer esto en nuestras joinStrings
:
¿Qué significa que "a" o "b" sean nulos?
Un valor de String
puede ser cero o más caracteres, por lo que ya tenemos una forma de representar una cadena vacía. ¿ null
significa algo diferente a ""
? Si no, es problemático tener dos formas de representar una cadena vacía.
¿El nulo vino de una variable no inicializada?
Un null
puede provenir de un campo no inicializado o de un elemento de matriz no inicializado. El valor podría no estar inicializado por diseño o por accidente. Si fue por accidente este es un error.
¿El nulo representa un "no se sabe" o "valor perdido"?
A veces una null
puede tener un significado genuino; por ejemplo, que el valor real de una variable es desconocido o no está disponible o es "opcional". En Java 8, la clase Optional
proporciona una mejor manera de expresar eso.
Si se trata de un error (o un error de diseño), ¿debemos "solucionarlo"?
Una interpretación del código es que estamos "arreglando" un null
inesperado al usar una cadena vacía en su lugar. ¿Es la estrategia correcta? ¿Sería mejor dejar que la NullPointerException
suceda, y luego capturar la excepción más arriba en la pila y registrarla como un error?
El problema con "hacer el bien" es que puede ocultar el problema o dificultar su diagnóstico.
¿Es esto eficiente / bueno para la calidad del código?
Si el enfoque de "hacer el bien" se usa constantemente, su código contendrá muchas pruebas nulas "defensivas". Esto va a hacer que sea más largo y más difícil de leer. Además, todas estas pruebas y "hacer el bien" pueden afectar el rendimiento de su aplicación.
En resumen
Si null
es un valor significativo, entonces la prueba para el caso null
es el enfoque correcto. El corolario es que si un valor null
es significativo, entonces esto debe documentarse claramente en los javadocs de cualquier método que acepte el valor null
o lo devuelva.
De lo contrario, es una mejor idea tratar un null
inesperado como un error de programación y dejar que se produzca la NullPointerException
para que el desarrollador sepa que hay un problema en el código.
Pitfall - Devolver nulo en lugar de lanzar una excepción
Algunos programadores de Java tienen una aversión general a lanzar o propagar excepciones. Esto conduce a un código como el siguiente:
public Reader getReader(String pathname) {
try {
return new BufferedReader(FileReader(pathname));
} catch (IOException ex) {
System.out.println("Open failed: " + ex.getMessage());
return null;
}
}
Entonces, ¿cuál es el problema con eso?
El problema es que getReader
está devolviendo un null
como un valor especial para indicar que no se pudo abrir el Reader
. Ahora se debe probar el valor devuelto para ver si es null
antes de usarlo. Si se omite la prueba, el resultado será una NullPointerException
.
En realidad hay tres problemas aquí:
- La
IOException
fue capturada demasiado pronto. - La estructura de este código significa que existe el riesgo de que se filtre un recurso.
- Luego se usó un
null
porque no había unReader
"real" disponible para devolver.
De hecho, asumiendo que la excepción debía ser detectada temprano de esta manera, había un par de alternativas para devolver null
:
- Sería posible implementar una clase
NullReader
; por ejemplo, una en la que las operaciones de API se comportan como si el lector ya estuviera en la posición de "final de archivo". - Con Java 8, sería posible declarar
getReader
como retornando unOptional<Reader>
.
Pitfall: no se comprueba si un flujo de E / S ni siquiera se inicializa al cerrarlo
Para evitar pérdidas de memoria, no se debe olvidar cerrar una secuencia de entrada o una secuencia de salida cuyo trabajo se haya realizado. Esto generalmente se hace con una declaración try
- catch
- finally
sin la parte catch
:
void writeNullBytesToAFile(int count, String filename) throws IOException {
FileOutputStream out = null;
try {
out = new FileOutputStream(filename);
for(; count > 0; count--)
out.write(0);
} finally {
out.close();
}
}
Si bien el código anterior puede parecer inocente, tiene un defecto que puede hacer que la depuración sea imposible. Si la línea en la out
se inicializa ( out = new FileOutputStream(filename)
) lanza una excepción, entonces out
será null
cuando out.close()
se ejecuta, lo que resulta en una desagradable NullPointerException
!
Para evitar esto, simplemente asegúrese de que el flujo no sea null
antes de intentar cerrarlo.
void writeNullBytesToAFile(int count, String filename) throws IOException {
FileOutputStream out = null;
try {
out = new FileOutputStream(filename);
for(; count > 0; count--)
out.write(0);
} finally {
if (out != null)
out.close();
}
}
Un enfoque aún mejor es try
-con-recursos, ya que se va a cerrar automáticamente la corriente con una probabilidad de 0 a lanzar una NPE sin la necesidad de un finally
de bloquear.
void writeNullBytesToAFile(int count, String filename) throws IOException {
try (FileOutputStream out = new FileOutputStream(filename)) {
for(; count > 0; count--)
out.write(0);
}
}
Pitfall: uso de la "notación Yoda" para evitar la excepción NullPointerException
Un montón de código de ejemplo publicado en StackOverflow incluye fragmentos como este:
if ("A".equals(someString)) {
// do something
}
Esto "evita" o "evita" una posible someString
NullPointerException
en el caso de que someString
sea null
. Además, es discutible que
"A".equals(someString)
es mejor que:
someString != null && someString.equals("A")
(Es más conciso y, en algunas circunstancias, podría ser más eficiente. Sin embargo, como argumentamos a continuación, la concisión podría ser negativa).
Sin embargo, el verdadero escollo es usar la prueba de Yoda para evitar NullPointerExceptions
como una cuestión de hábito.
Cuando escribes "A".equals(someString)
realidad estás "haciendo bien" el caso en el que sucede que someString
son null
. Pero como lo explica otro ejemplo ( Pitfall - "Hacer buenos" nulos inesperados ), "Hacer buenos" valores null
puede ser perjudicial por una variedad de razones.
Esto significa que las condiciones de Yoda no son "mejores prácticas" 1 . A menos que se espere el null
, es mejor dejar que ocurra la NullPointerException
para que pueda obtener una falla de prueba de la unidad (o un informe de error). Eso le permite encontrar y corregir el error que provocó que apareciera un null
inesperado / no deseado.
Las condiciones de Yoda solo deben usarse en los casos en que se espera el null
porque el objeto que está probando proviene de una API que está documentada como que devuelve un null
. Y podría decirse que podría ser mejor usar una de las formas menos bonitas que expresan la prueba porque eso ayuda a resaltar la prueba null
para alguien que está revisando su código.
1 - Según Wikipedia : "Las mejores prácticas de codificación son un conjunto de reglas informales que la comunidad de desarrollo de software ha aprendido a lo largo del tiempo y puede ayudar a mejorar la calidad del software". . El uso de la notación Yoda no logra esto. En muchas situaciones, empeora el código.