¿Por qué lanzar explícitamente una NullPointerException en lugar de dejar que ocurra naturalmente?

182

Al leer el código fuente JDK, encuentro común que el autor verificará los parámetros si son nulos y luego lanzará una nueva NullPointerException () manualmente. ¿Por qué lo hacen? Creo que no es necesario hacerlo, ya que arrojará una nueva NullPointerException () cuando llame a cualquier método. (Aquí hay un código fuente de HashMap, por ejemplo :)

public V computeIfPresent(K key,
                          BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
    if (remappingFunction == null)
        throw new NullPointerException();
    Node<K,V> e; V oldValue;
    int hash = hash(key);
    if ((e = getNode(hash, key)) != null &&
        (oldValue = e.value) != null) {
        V v = remappingFunction.apply(key, oldValue);
        if (v != null) {
            e.value = v;
            afterNodeAccess(e);
            return v;
        }
        else
            removeNode(hash, key, null, false, true);
    }
    return null;
}
LiJiaming
fuente
32
un punto clave de la codificación es la intención
Scary Wombat
19
¡Esta es una muy buena pregunta para tu primera pregunta! He hecho algunas ediciones menores; Espero que no te moleste. También eliminé el agradecimiento y la nota sobre que es su primera pregunta, ya que normalmente ese tipo de cosas no forma parte de las preguntas SO.
David Conrad
11
Soy C #, la convención es plantear ArgumentNullExceptionen casos como ese (en lugar de NullReferenceException): en realidad es una muy buena pregunta de por qué plantearías NullPointerExceptionexplícitamente aquí (en lugar de una diferente).
EJoshuaS - Restablece a Monica
21
@EJoshuaS Es un viejo debate si lanzar IllegalArgumentExceptiono NullPointerExceptionpor un argumento nulo. La convención JDK es la última.
shmosel
33
El problema REAL es que arrojan un error y tiran toda la información que condujo a este error . Parece que este es el código fuente real . Ni siquiera un simple mensaje de cadena sangrienta. Triste.
Martin Ba

Respuestas:

254

Hay una serie de razones que me vienen a la mente, varias de ellas estrechamente relacionadas:

Fallo rápido: si va a fallar, lo mejor es fallar más temprano que tarde. Esto permite detectar los problemas más cerca de su origen, lo que facilita su identificación y recuperación. También evita desperdiciar los ciclos de CPU en el código que seguramente fallará.

Intención: Lanzar la excepción explícitamente deja en claro a los mantenedores que el error está allí a propósito y que el autor estaba al tanto de las consecuencias.

Consistencia: si se permitiera que el error ocurriera naturalmente, podría no ocurrir en todos los escenarios. Si no se encuentra ninguna asignación, por ejemplo, remappingFunctionnunca se usaría y la excepción no se lanzaría. Validar la entrada por adelantado permite un comportamiento más determinista y una documentación más clara .

Estabilidad: el código evoluciona con el tiempo. El código que encuentra una excepción, naturalmente, podría, después de un poco de refactorización, dejar de hacerlo, o hacerlo en diferentes circunstancias. Lanzarlo explícitamente hace que sea menos probable que el comportamiento cambie inadvertidamente.

shmosel
fuente
14
Además: de esta manera, la ubicación desde donde se produce la excepción está vinculada a exactamente una variable que se verifica. Sin ella, la excepción podría deberse a que una de las múltiples variables es nula.
Jens Schauder
45
Otro: si espera que NPE ocurra naturalmente, es posible que algún código intermedio ya haya cambiado el estado de su programa a través de los efectos secundarios.
Thomas
66
Si bien este fragmento no lo hace, podría usar el new NullPointerException(message)constructor para aclarar qué es nulo. Bueno para personas que no tienen acceso a su código fuente. Incluso hicieron de esto una línea en JDK 8 con el Objects.requireNonNull(object, message)método de utilidad.
Robin
3
La FALLA debe estar cerca de la FALLA. "Fail Fast" es más que una regla general. ¿Cuándo no quieres este comportamiento? Cualquier otro comportamiento significa que está ocultando errores. Hay "FALLAS" y "FALLAS". El FALLO es cuando este programa digiere un puntero NULO y se bloquea. Pero esa línea de código no es donde se encuentra la FALLA. El NULL vino de alguna parte: un argumento de método. ¿Quién pasó esa discusión? Desde alguna línea de código que hace referencia a una variable local. ¿Dónde hizo eso ... Ves? Eso apesta. ¿De quién fue la responsabilidad de ver almacenado un valor malo? Su programa debería haberse bloqueado entonces.
Noah Spurrier
44
@Thomas Buen punto. Shmosel: El punto de Thomas puede estar implícito en el punto de falla rápida, pero está algo enterrado. Es un concepto suficientemente importante que tiene su propio nombre: atomicidad de falla . Ver Bloch, Effective Java , Item 46. Tiene una semántica más fuerte que la falla rápida. Sugeriría llamarlo en un punto separado. Excelente respuesta en general, por cierto. +1
Stuart Marks
40

Es por claridad, consistencia y para evitar que se realice trabajo adicional e innecesario.

Considere lo que sucedería si no hubiera una cláusula de guardia en la parte superior del método. Siempre llamaba hash(key)e getNode(hash, key)incluso cuando se nullhabía pasado remappingFunctionantes de que se lanzara el NPE.

Peor aún, si la ifcondición es falseentonces, tomamos la elserama, que no usa remappingFunctionen absoluto, lo que significa que el método no siempre arroja NPE cuando nullse pasa a; si lo hace depende del estado del mapa.

Ambos escenarios son malos. Si nullno es un valor válido para remappingFunctionel método, debe arrojar una excepción de manera consistente, independientemente del estado interno del objeto en el momento de la llamada, y debe hacerlo sin hacer un trabajo innecesario que no tiene sentido dado que solo se lanzará. Finalmente, es un buen principio de código limpio y claro contar con la protección por adelantado para que cualquiera que revise el código fuente pueda ver fácilmente que lo hará.

Incluso si la excepción fuera lanzada actualmente por cada rama del código, es posible que una futura revisión del código cambie eso. Realizar la verificación al principio asegura que definitivamente se realizará.

David Conrad
fuente
25

Además de las razones enumeradas por la excelente respuesta de @ shmosel ...

Rendimiento: puede haber / ha habido beneficios de rendimiento (en algunas JVM) al lanzar el NPE explícitamente en lugar de dejar que la JVM lo haga.

Depende de la estrategia que tome el intérprete de Java y el compilador JIT para detectar la desreferenciación de punteros nulos. Una estrategia es no probar nulo, sino atrapar el SIGSEGV que ocurre cuando una instrucción intenta acceder a la dirección 0. Este es el enfoque más rápido en el caso donde la referencia siempre es válida, pero es costosa en el caso de NPE.

Una prueba explícita nullen el código evitaría el impacto en el rendimiento de SIGSEGV en un escenario donde los NPE eran frecuentes.

(Dudo que sea una microoptimización que valga la pena en una JVM moderna, pero podría haber sido en el pasado).


Compatibilidad: la razón probable de que no haya ningún mensaje en la excepción es la compatibilidad con los NPE que arroja la propia JVM. En una implementación Java compatible, un NPE lanzado por la JVM tiene un nullmensaje. (Android Java es diferente).

Stephen C
fuente
20

Además de lo que otras personas han señalado, vale la pena señalar el papel de la convención aquí. En C #, por ejemplo, también tiene la misma convención de plantear explícitamente una excepción en casos como este, pero es específicamente una ArgumentNullException, que es algo más específica. (La convención de C # es que NullReferenceException siempre representa un error de algún tipo; simplemente, no debería suceder nunca en el código de producción; concedido, ArgumentNullExceptionpor lo general también lo hace, pero podría ser un error más en la línea de "usted no entiendo cómo usar la biblioteca correctamente "tipo de error).

Entonces, básicamente, en C # NullReferenceExceptionsignifica que su programa realmente trató de usarlo, mientras ArgumentNullExceptionque significa que reconoció que el valor era incorrecto y ni siquiera se molestó en tratar de usarlo. Las implicaciones pueden ser realmente diferentes (dependiendo de las circunstancias) porque ArgumentNullExceptionsignifica que el método en cuestión aún no tuvo efectos secundarios (ya que falló las condiciones previas del método).

Por cierto, si está planteando algo como ArgumentNullExceptiono IllegalArgumentException, eso es parte del punto de hacer la verificación: desea una excepción diferente de la que "normalmente" obtendría.

De cualquier manera, aumentar explícitamente la excepción refuerza la buena práctica de ser explícito sobre las condiciones previas y los argumentos esperados de su método, lo que hace que el código sea más fácil de leer, usar y mantener. Si no lo verificó explícitamente null, no sé si es porque pensó que nadie pasaría una nulldiscusión, está contando que arroje la excepción de todos modos, o simplemente se olvidó de verificar eso.

EJoshuaS - Restablece a Monica
fuente
44
+1 para el párrafo del medio. Yo diría que el código en cuestión debería 'lanzar una nueva IllegalArgumentException ("la función de reasignación no puede ser nula"); De esa manera, es inmediatamente evidente lo que estaba mal. El NPE que se muestra es un poco ambiguo.
Chris Parker
1
@ChrisParker Solía ​​ser de la misma opinión, pero resulta que NullPointerException está destinado a significar un argumento nulo pasado a un método que espera un argumento no nulo, además de ser una respuesta en tiempo de ejecución a un intento de desreferenciar nulo. Del javadoc: "Las aplicaciones deberían lanzar instancias de esta clase para indicar otros usos ilegales del nullobjeto". No estoy loco por eso, pero ese parece ser el diseño previsto.
VGR
1
Estoy de acuerdo, @ChrisParker: creo que esa excepción es más específica (porque el código nunca intentó hacer nada con el valor, inmediatamente reconoció que no debería usarlo). Me gusta la convención de C # en este caso. La convención de C # es que NullReferenceException(equivalente a NullPointerException) significa que su código realmente intentó usarlo (que siempre es un error, nunca debería suceder en el código de producción), vs. "Sé que el argumento está mal, así que ni siquiera intenta usarlo ". También hay ArgumentException(lo que significa que el argumento fue incorrecto por alguna otra razón).
EJoshuaS - Restablece a Monica
2
Diré esto mucho, SIEMPRE lanzo una IllegalArgumentException como se describe. Siempre me siento cómodo burlándome de las convenciones cuando siento que la convención es tonta.
Chris Parker
1
@PieterGeerkens: sí, porque la línea 35 de NullPointerException es MUCHO mejor que la línea 35 IllegalArgumentException ("La función no puede ser nula"). ¿En serio?
Chris Parker
12

Es así que obtendrá la excepción tan pronto como cometa el error, en lugar de más adelante cuando esté usando el mapa y no entienda por qué sucedió.

Marqués de Lorne
fuente
9

Convierte una condición de error aparentemente errática en una violación clara del contrato: la función tiene algunas condiciones previas para funcionar correctamente, por lo que las verifica de antemano y exige que se cumplan.

El efecto es que no tendrá que depurar computeIfPresent()cuando obtenga la excepción. Una vez que vea que la excepción proviene de la verificación de precondición, sabe que llamó a la función con un argumento ilegal. Si la verificación no estuviera allí, deberá excluir la posibilidad de que haya algún error en computeIfPresent()sí mismo que provoque la excepción.

Obviamente, lanzar el genérico NullPointerExceptiones una muy mala elección, ya que no indica una violación del contrato en sí misma. IllegalArgumentExceptionSería una mejor opción.


Nota al margen:
no sé si Java permite esto (lo dudo), pero los programadores de C / C ++ usan un assert()en este caso, que es significativamente mejor para la depuración: le dice al programa que se bloquee de inmediato y lo más duro posible si se proporciona condición evaluar a falso. Entonces, si corriste

void MyClass_foo(MyClass* me, int (*someFunction)(int)) {
    assert(me);
    assert(someFunction);

    ...
}

debajo de un depurador, y algo pasó NULLa cualquiera de los argumentos, el programa se detendría justo en la línea que indica qué argumento era NULL, y usted podría examinar todas las variables locales de toda la pila de llamadas en el tiempo libre.

cmaster - restablecer monica
fuente
1
assert something != null;pero eso requiere la -assertionsbandera cuando se ejecuta la aplicación. Si la -assertionsbandera no está allí, la palabra clave de aserción no arrojará una Excepción de Afirmación
Zoe
Estoy de acuerdo, por eso prefiero la convención de C # aquí: la referencia nula, el argumento inválido y el argumento nulo generalmente implican un error de algún tipo, pero implican diferentes tipos de errores. "Estás tratando de usar una referencia nula" a menudo es muy diferente a "estás haciendo un mal uso de la biblioteca".
EJoshuaS - Restablece a Monica
7

Es porque es posible que no suceda naturalmente. Veamos un código como este:

bool isUserAMoron(User user) {
    Connection c = UnstableDatabase.getConnection();
    if (user.name == "Moron") { 
      // In this case we don't need to connect to DB
      return true;
    } else {
      return c.makeMoronishCheck(user.id);
    }
}

(por supuesto, hay numerosos problemas en esta muestra sobre la calidad del código. Perdón por la pereza de imaginar una muestra perfecta)

Situación en la cque no se utilizará realmente y NullPointerExceptionno se lanzará, incluso si c == nulles posible.

En situaciones más complicadas, resulta muy difícil cazar esos casos. Esta es la razón por la cual la verificación general if (c == null) throw new NullPointerException()es mejor.

Arenim
fuente
Podría decirse que una pieza de código que funciona sin una conexión de base de datos cuando realmente no es necesaria es algo bueno, y el código que se conecta a una base de datos solo para ver si puede hacerlo es bastante molesto.
Dmitry Grigoryev
5

Es intencional proteger más daños o entrar en un estado inconsistente.

Fairoz
fuente
1

Además de todas las otras respuestas excelentes aquí, también me gustaría agregar algunos casos.

Puede agregar un mensaje si crea su propia excepción

Si arrojas el NullPointerExceptiontuyo, puedes agregar un mensaje (¡que definitivamente deberías!)

El mensaje predeterminado es nullde new NullPointerException()y todos los métodos que lo usan, por ejemplo Objects.requireNonNull. Si imprime ese nulo, incluso puede traducirse a una cadena vacía ...

Un poco corto y poco informativo ...

El seguimiento de la pila dará mucha información, pero para que el usuario sepa qué era nulo, debe desenterrar el código y mirar la fila exacta.

Ahora imagine que NPE se envuelve y se envía a través de la red, por ejemplo, como un mensaje en un error del servicio web, tal vez entre diferentes departamentos o incluso organizaciones. En el peor de los casos, nadie puede descubrir qué nullsignifica ...

Las llamadas a métodos encadenados te mantendrán adivinando

Una excepción solo le indicará en qué fila se produjo la excepción. Considere la siguiente fila:

repository.getService(someObject.someMethod());

Si recibe un NPE y apunta en esta fila, uno de los cuales repositoryy someObjectfue nula?

En cambio, al verificar estas variables cuando las obtiene, al menos apuntará a una fila donde, con suerte, son la única variable que se maneja. Y, como se mencionó anteriormente, aún mejor si su mensaje de error contiene el nombre de la variable o similar.

Los errores al procesar muchas entradas deben proporcionar información de identificación

Imagine que su programa está procesando un archivo de entrada con miles de filas y de repente hay una NullPointerException. Miras el lugar y te das cuenta de que alguna entrada fue incorrecta ... ¿qué entrada? Necesitará más información sobre el número de fila, tal vez la columna o incluso el texto de la fila completa para comprender qué fila en ese archivo necesita corrección.

Erk
fuente