java - ¿Es una mala idea si equals(null) arroja NullPointerException en su lugar?
(11)
A la pregunta de si esta asimetría es inconsistente, creo que no, y los remito a este antiguo Zen Koan:
- Pregúntale a cualquier hombre si es tan bueno como el próximo hombre y cada uno dirá que sí.
- Pregúntele a cualquier hombre si es tan bueno como nadie y cada uno dirá que no.
- No preguntes a nadie si es tan bueno como cualquier hombre y nunca recibirás una respuesta.
En ese momento, el compilador alcanzó la iluminación.
El contrato de equals
con respecto a null
, es el siguiente:
Para cualquier valor de referencia no nulo
x
,x.equals(null)
deberíareturn false
.
Esto es bastante peculiar, porque si o1 != null
y o2 == null
, entonces tenemos:
o1.equals(o2) // returns false
o2.equals(o1) // throws NullPointerException
El hecho de que o2.equals(o1) throws NullPointerException
es algo bueno, ya que nos avisa del error del programador. Y, sin embargo, ese error no sería atrapado si por varias razones simplemente lo cambiamos a o1.equals(o2)
, que simplemente "fallaría silenciosamente".
Entonces las preguntas son:
- ¿Por qué es una buena idea que
o1.equals(o2)
return false
lugar de arrojarNullPointerException
? - ¿Sería una mala idea si, de ser posible, reescribimos el contrato para que
anyObject.equals(null)
siempre arrojeNullPointerException
lugar?
En comparación con Comparable
Por el contrario, esto es lo que dice el contrato Comparable
:
Tenga en cuenta que
null
no es una instancia de ninguna clase, ye.compareTo(null)
debería arrojar unaNullPointerException
aunquee.equals(null)
devuelvefalse
.
Si NullPointerException
es apropiado para compareTo
, ¿por qué no es para equals
?
Preguntas relacionadas
Un argumento puramente semántico
Estas son las palabras reales en la documentación equals :
Indica si algún otro objeto es "igual a" este.
¿Y qué es un objeto?
JLS 4.3.1 Objetos
Un objeto es una instancia de clase o una matriz.
Los valores de referencia (a menudo solo referencias ) son punteros a estos objetos, y una referencia
null
especial, que se refiere a ningún objeto .
Mi argumento desde este ángulo es realmente simple.
-
equals
probar si algún otro objeto es "igual a"this
- referencia
null
no proporciona ningún otro objeto para la prueba - Por lo tanto,
equals(null)
debería lanzarNullPointerException
Creo que se trata de conveniencia y, lo que es más importante, de consistencia: permitir que los nulos sean parte de la comparación evita tener que hacer una comprobación null
e implementar la semántica de cada vez que se llame a equals
. null
referencias null
son legales en muchos tipos de colecciones, por lo que tiene sentido que puedan aparecer como el lado derecho de la comparación.
El uso de métodos de instancia para la igualdad, la comparación, etc., necesariamente hace que la disposición sea asimétrica, una pequeña molestia para la enorme ganancia de polimorfismo. Cuando no necesito polimorfismo, a veces creo un método estático simétrico con dos argumentos, MyObject.equals(MyObjecta, MyObject b)
. Este método luego verifica si uno o ambos argumentos son referencias nulas. Si específicamente deseo excluir referencias nulas, entonces creo un método adicional, por ejemplo, equalsStrict()
o similar, que hace una comprobación nula antes de delegar en el otro método.
En el primer caso, o1.equals(o2)
devuelve falso porque o1
no es igual a o2
, lo cual está perfectamente bien. En el segundo caso, arroja NullPointerException
porque o2
es null
. Uno no puede llamar a ningún método en un null
. Puede ser una limitación de los lenguajes de programación en general, pero tenemos que vivir con eso.
Tampoco es una buena idea lanzar NullPointerException
porque está violando el contrato para el método equals
y haciendo que las cosas sean más complejas de lo que deben ser.
Esta es una pregunta difícil. Para compatibilidad hacia atrás no puedes hacerlo.
Imagine el siguiente escenario
void m (Object o) {
if (one.equals (o)) {}
else if (two.equals (o)) {}
else {}
}
Ahora con iguales devolviendo falso, la cláusula else se ejecutará, pero no cuando se lanza una excepción.
También null no es realmente igual a decir "2", por lo que tiene mucho sentido devolver el resultado falso. Entonces, probablemente sea mejor insistir en que null.equals ("b") también devuelva falso :))
Pero este requisito sí crea una relación equitativa extraña y no simétrica.
Hay muchas situaciones comunes donde null
no es de ninguna manera excepcional, por ejemplo, simplemente puede representar el caso (no excepcional) en el que una clave no tiene ningún valor, o de lo contrario no significa "nada". Por lo tanto, hacer x.equals(y)
con un y
desconocido también es bastante común, y tener que comprobar siempre primero para null
sería solo un esfuerzo desperdiciado.
En cuanto a por qué null.equals(y)
es diferente, es un error de programación llamar a cualquier método de instancia en una referencia nula en Java y, por lo tanto, merece una excepción. El orden de y
en x.equals(y)
debe elegirse de modo que se sepa que x
no es null
. Yo diría que, en casi todos los casos, este reordenamiento se puede hacer en base a lo que se conoce sobre los objetos de antemano (por ejemplo, desde su origen, o al comparar con el null
para otras llamadas a métodos).
Mientras tanto, si ambos objetos son de "nulidad" desconocida, entonces es casi seguro que otro código requiera verificar al menos uno de ellos, o no se puede hacer mucho con el objeto sin arriesgar la NullPointerException
.
Y como esta es la forma en que se especifica, es un error de programación romper el contrato y generar una excepción para que un argumento null
equals
. Y si considera la alternativa de requerir que se genere una excepción, entonces cada implementación de equals
tendría que hacer un caso especial, y cada llamada a equals
con cualquier objeto potencialmente null
tendría que verificar antes de llamar.
Podría haber sido especificado de manera diferente (es decir, la condición previa de los equals
requeriría que el argumento no sea null
), por lo tanto, esto no significa que su argumentación no es válida, pero la especificación actual permite un lenguaje de programación más simple y práctico.
No es que esto sea necesariamente una respuesta a su pregunta, es solo un ejemplo de cuando me parece útil que el comportamiento sea como es ahora.
private static final String CONSTANT_STRING = "Some value";
String text = getText(); // Whatever getText() might be, possibly returning null.
Tal como está, puedo hacerlo.
if (CONSTANT_STRING.equals(text)) {
// do something.
}
Y no tengo ninguna posibilidad de obtener una NullPointerException. Si se cambió como sugirió, volvería a tener que hacer lo siguiente:
if (text != null && text.equals(CONSTANT_STRING)) {
// do something.
}
¿Es esta una buena razón para que el comportamiento sea como es? No lo sé, pero es un efecto secundario útil.
Personalmente, prefiero que funcione como lo hace.
La NullPointerException
identifica que el problema está en el objeto contra el cual se realiza la operación de igualdad.
Si la NullPointerException
se usó como sugieres e intentaste la operación (algo así como inútil) de ...
o1.equals(o1)
donde o1 = null ... ¿Se lanza la NullPointerException
porque la función de comparación está jodida o porque o1 es nula pero no se dio cuenta? Un ejemplo extremo, lo sé, pero con el comportamiento actual, siento que puedes decir fácilmente dónde está el problema.
Piense en cómo .equals está relacionado con == y .compareTo está relacionado con los operadores de comparación>, <,> =, <=.
Si va a argumentar que el uso de .equals para comparar un objeto con un valor nulo debería arrojar un NPE, entonces tendría que decir que este código debería arrojar uno también:
Object o1 = new Object();
Object o2 = null;
boolean b = (o1 == o2); // should throw NPE here!
La diferencia entre o1.equals (o2) y o2.equals (o1) es que en el primer caso está comparando algo con null, similar a o1 == o2, mientras que en el segundo caso, el método equals nunca se ejecuta realmente así que no hay comparación en absoluto.
En cuanto al contrato .compareTo, comparar un objeto no nulo con un objeto nulo es como intentar hacer esto:
int j = 0;
if(j > null) {
...
}
Obviamente esto no compilará. Puedes usar el auto-unboxing para hacer que se compile, pero obtienes un NPE cuando haces la comparación, que es consistente con el contrato .compareTo:
Integer i = null;
int j = 0;
if(j > i) { // NPE
...
}
Si se tienen en cuenta los conceptos orientados a objetos, y se consideran los roles de todo emisor y receptor, diría que el comportamiento es conveniente. Vea en el primer caso que está preguntando a un objeto si no es igual a nadie. Él DEBE decir "NO, yo no".
En el segundo caso, sin embargo, no tienes una referencia con nadie. Por lo tanto, realmente no estás preguntando a nadie. ESTO debería arrojar una excepción, el primer caso no debería.
Creo que es solo asimétrico si te olvidas de la orientación a objetos y tratas la expresión como una igualdad matemática. Sin embargo, en este paradigma ambos extremos desempeñan diferentes roles, por lo que es de esperar que el orden sea importante.
Como un punto final. Se debe generar una excepción de puntero nulo cuando hay un error en su código. Sin embargo, preguntarle a un objeto si no es nadie, no se debe considerar una falla de programación. Creo que está bien preguntarle a un objeto si no es nulo. ¿Qué pasa si no controlas la fuente que te proporciona el objeto? y esta fuente te envía nulo. ¿Verificaría si el objeto es nulo y solo después verá si son iguales? ¿No sería más intuitivo simplemente comparar los dos y cualquiera que sea el segundo objeto es que la comparación se llevará a cabo sin excepciones?
Honestamente, me enojaría si un método igual dentro de su cuerpo devuelve una excepción de puntero nulo a propósito. Equals está destinado a ser utilizado contra cualquier tipo de objeto, por lo que no debe ser tan exigente con lo que recibe. Si un método equals devolviera npe, lo último en mi mente sería que lo hizo a propósito. Especialmente considerando que es una excepción no verificada. SI levantaras una npe, un chico tendría que recordar comprobar siempre nulo antes de llamar a tu método, o peor aún, rodear la llamada a iguales en un bloque try / catch (Dios odio probar / atrapar bloques) Pero bueno. ..
Tenga en cuenta que el contrato es "para cualquier referencia x no nula". Entonces la implementación se verá así:
if (x != null) {
if (x.equals(null)) {
return false;
}
}
x
no necesita ser null
para ser considerado igual a null
porque la siguiente definición de equals
es posible:
public boolean equals(Object obj) {
// ...
// If someMember is 0 this object is considered as equal to null.
if (this.someMember == 0 and obj == null) {
return true;
}
return false;
}
Una excepción realmente debería ser una situación excepcional . Un puntero nulo podría no ser un error del programador.
Usted citó el contrato existente. Si decides ir en contra de la convención, después de todo este tiempo, cuando todos los desarrolladores de Java esperen que los iguales devuelvan falso, estarás haciendo algo inesperado y desagradable que convertirá a tu clase en paria.
No puedo estar más en desacuerdo. No volvería a escribir iguales para lanzar una excepción todo el tiempo. Reemplazaría cualquier clase que hiciera eso si fuera su cliente.