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 bloque synchronized ,
  • 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 de Map devolverá un null si lo llama con una clave que no tiene una asignación.
  • Los getResource(path) y getResourceAsStream(path) en las API de ClassLoader y Class devolverán el null si no se puede encontrar el recurso.
  • El método get() en la API de Reference devolverá un null si el recolector de basura ha borrado la referencia.
  • Varios métodos getXxxx en las API del servlet de Java EE devolverán un null 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 y d deben ser envoltorios primitivos, entonces no debemos confiar en la inicialización predeterminada, o deberíamos estar comprobando que no null . Para anterior es el enfoque correcto a menos que haya un significado definido para los campos en estado null .

  • 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í:

  1. La IOException fue capturada demasiado pronto.
  2. La estructura de este código significa que existe el riesgo de que se filtre un recurso.
  3. Luego se usó un null porque no había un Reader "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 :

  1. 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".
  2. Con Java 8, sería posible declarar getReader como retornando un Optional<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.



Modified text is an extract of the original Stack Overflow Documentation
Licenciado bajo CC BY-SA 3.0
No afiliado a Stack Overflow