Le contrat de equals
en ce qui concerne null
, est le suivant:
Pour toute valeur de référence non nulle
x
,x.equals(null)
devraitreturn false
.
C'est assez particulier, car si o1 != null
Et o2 == null
, Alors nous avons:
o1.equals(o2) // returns false
o2.equals(o1) // throws NullPointerException
Le fait que o2.equals(o1) throws NullPointerException
soit une bonne chose, car il nous avertit d'une erreur de programmation. Et pourtant, cette erreur ne serait pas détectée si, pour diverses raisons, nous la commutions simplement sur o1.equals(o2)
, ce qui échouerait simplement à la place.
Les questions sont donc:
o1.equals(o2)
devrait return false
Au lieu de lancer NullPointerException
?anyObject.equals(null)
lance toujours NullPointerException
à la place?Comparable
En revanche, c'est ce que dit le Comparable
contrat :
Notez que
null
n'est une instance d'aucune classe, ete.compareTo(null)
doit lancer unNullPointerException
même sie.equals(null)
renvoiefalse
.
Si NullPointerException
est approprié pour compareTo
, pourquoi ne l'est-il pas pour equals
?
Ce sont les mots réels dans la documentation Object.equals(Object obj)
:
Indique si certains autres objets sont "égaux" à celui-ci.
Et qu'est-ce qu'un objet?
Un objet est une instance de classe ou un tableau.
Les valeurs de référence (souvent juste références ) sont des pointeurs vers ces objets, et une référence spéciale
null
, qui ne fait référence à aucun objet .
Mon argument sous cet angle est vraiment simple.
equals
teste si certains autres objets sont "égaux" this
null
la référence ne donne aucun autre objet pour le testequals(null)
devrait lancer NullPointerException
À la question de savoir si cette asymétrie est incohérente, je ne le pense pas, et je vous renvoie à cet ancien kōan zen:
À ce moment, le compilateur a atteint Enlightenment.
Une exception devrait vraiment être une situation exceptionnelle. Un pointeur nul n'est peut-être pas une erreur de programmeur.
Vous avez cité le contrat existant. Si vous décidez d'aller à l'encontre de la convention, après tout ce temps, lorsque chaque développeur Java s'attend à ce que equals retourne false, vous ferez quelque chose d'inattendu et de malvenu qui fera de votre classe un paria.
Je ne pourrais pas être plus en désaccord. Je ne réécrirais pas égal à jeter une exception tout le temps. Je remplacerais n'importe quelle classe qui ferait ça si j'étais son client.
Pensez à la façon dont .equals est lié à == et .compareTo est lié aux opérateurs de comparaison>, <,> =, <=.
Si vous allez faire valoir que l'utilisation de .equals pour comparer un objet à null devrait lancer un NPE, alors vous devez dire que ce code devrait également en lancer un:
Object o1 = new Object();
Object o2 = null;
boolean b = (o1 == o2); // should throw NPE here!
La différence entre o1.equals (o2) et o2.equals (o1) est que dans le premier cas vous comparez quelque chose à null, similaire à o1 == o2, tandis que dans le second cas, la méthode equals n'est jamais réellement exécuté il n'y a donc aucune comparaison.
Concernant le contrat .compareTo, comparer un objet non nul avec un objet nul revient à essayer de faire ceci:
int j = 0;
if(j > null) {
...
}
Évidemment, cela ne se compilera pas. Vous pouvez utiliser la décompression automatique pour la compiler, mais vous obtenez un NPE lorsque vous effectuez la comparaison, ce qui est cohérent avec le contrat .compareTo:
Integer i = null;
int j = 0;
if(j > i) { // NPE
...
}
Non pas que ce soit nécessairement une réponse à votre question, c'est juste un exemple où je trouve utile que le comportement soit tel qu'il est maintenant.
private static final String CONSTANT_STRING = "Some value";
String text = getText(); // Whatever getText() might be, possibly returning null.
En l'état, je peux le faire.
if (CONSTANT_STRING.equals(text)) {
// do something.
}
Et je n'ai aucune chance d'obtenir une NullPointerException. Si elle était modifiée comme vous l'avez suggéré, je serais de retour à faire:
if (text != null && text.equals(CONSTANT_STRING)) {
// do something.
}
Est-ce une raison suffisante pour que le comportement soit tel qu'il est ?? Je ne sais pas, mais c'est un effet secondaire utile.
Si vous tenez compte des concepts orientés objet et considérez l'ensemble des rôles d'expéditeur et de récepteur, je dirais que le comportement est pratique. Voyez dans le premier cas, vous demandez à un objet s'il n'est égal à personne. Il DEVRAIT dire "NON, je ne le suis pas".
Dans le deuxième cas cependant, vous n'avez de référence à personne. Vous ne demandez donc rien à personne. CECI devrait lever une exception, le premier cas ne devrait pas.
Je pense que ce n'est asymétrique que si vous oubliez l'orientation de l'objet et traitez l'expression comme une égalité mathématique. Cependant, dans ce paradigme, les deux extrémités jouent des rôles différents, il faut donc s'attendre à ce que l'ordre compte.
Comme dernier point. Une exception de pointeur nul doit être déclenchée en cas d'erreur dans votre code. Cependant, demander un objet s'il n'est personne ne devrait pas être considéré comme un défaut de programmation. Je pense que c'est tout à fait correct de demander à un objet s'il n'est pas nul. Et si vous ne contrôlez pas la source qui vous fournit l'objet? et cette source vous envoie null. Souhaitez-vous vérifier si l'objet est nul et voir ensuite seulement s'ils sont égaux? Ne serait-il pas plus intuitif de simplement comparer les deux et quel que soit le deuxième objet, la comparaison sera effectuée sans exception?
En toute honnêteté, je serais énervé si une méthode d'égalité dans son corps renvoie une exception de pointeur nul exprès. Equals est destiné à être utilisé contre tout type d'objet, il ne devrait donc pas être aussi pointilleux sur ce qu'il reçoit. Si une méthode equals retournait npe, la dernière chose dans mon esprit serait qu'elle l'ait fait exprès. En particulier, il s'agit d'une exception non vérifiée. SI vous avez élevé un npe, un gars devrait se rappeler de toujours vérifier la valeur null avant d'appeler votre méthode, ou pire encore, entourez l'appel à égal dans un bloc try/catch (Dieu je déteste les blocs try/catch) Mais bon. ..
Personnellement, je préfère qu'il fonctionne comme il le fait.
Le NullPointerException
identifie que le problème est dans l'objet contre lequel l'opération égale est effectuée.
Si le NullPointerException
a été utilisé comme vous le suggérez et que vous avez essayé l'opération (en quelque sorte inutile) de ...
o1.equals(o1)
où o1 = null ... Le NullPointerException
est-il lancé parce que votre fonction de comparaison est vissée ou parce que o1 est nul mais que vous ne vous en êtes pas rendu compte? Un exemple extrême, je sais, mais avec le comportement actuel, je pense que vous pouvez facilement dire où se situe le problème.
Dans le premier cas, o1.equals(o2)
renvoie false car o1
n'est pas égal à o2
, ce qui est parfaitement bien. Dans le second cas, il jette NullPointerException
parce que o2
est null
. On ne peut appeler aucune méthode sur un null
. Cela peut être une limitation des langages de programmation en général, mais nous devons vivre avec.
Ce n'est pas non plus une bonne idée de lancer NullPointerException
vous violez le contrat pour la méthode equals
et rendez les choses plus complexes qu'elles ne doivent l'être.
Il existe de nombreuses situations courantes où null
n'est en rien exceptionnel, par exemple il peut simplement représenter le cas (non exceptionnel) où une clé n'a aucune valeur, ou autrement représenter "rien". Par conséquent, faire x.equals(y)
avec un y
inconnu est également assez courant, et devoir toujours vérifier null
en premier serait un effort inutile.
Quant à savoir pourquoi null.equals(y)
est différent, c'est is une erreur de programmation pour appeler any méthode d'instance sur une référence nulle en Java , et donc digne d'une exception. L'ordre de x
et y
dans x.equals(y)
doit être choisi de telle sorte que x
soit connu pour ne pas être null
. Je dirais que dans presque tous les cas, cette réorganisation peut être effectuée sur la base de ce qui est connu des objets au préalable (par exemple, à partir de leur origine, ou en vérifiant dans null
pour d'autres appels de méthode).
Pendant ce temps, si les deux objets sont de "nullité" inconnue, un autre code nécessite presque certainement de vérifier au moins l'un d'entre eux, ou peu de choses peuvent être faites avec l'objet sans risquer le NullPointerException
.
Et puisque c'est la façon dont il est spécifié, c'est une erreur de programmation de rompre le contrat et de lever une exception pour un argument null
à equals
. Et si vous envisagez l'alternative d'exiger la levée d'une exception, chaque implémentation de equals
devrait en faire un cas particulier, et chaque appel à equals
avec un éventuel null
objet devrait vérifier avant d'appeler.
Il pourrait avoir été spécifié différemment (c'est-à-dire que la précondition de equals
nécessiterait que l'argument soit non -null
), donc cela ne veut pas dire que votre argumentation n'est pas valide, mais la spécification actuelle permet un langage de programmation plus simple et plus pratique.
Je pense qu'il s'agit de commodité et, plus important encore, de cohérence - permettre aux valeurs nulles de faire partie de la comparaison évite d'avoir à vérifier null
et à implémenter la sémantique de cela chaque fois que equals
est appelé. null
les références sont légales dans de nombreux types de collections, il est donc logique qu'elles puissent apparaître comme le côté droit de la comparaison.
L'utilisation de méthodes d'instance pour l'égalité, la comparaison, etc., rend nécessairement l'arrangement asymétrique - un petit tracas pour l'énorme gain de polymorphisme. Quand je n'ai pas besoin de polymorphisme, je crée parfois une méthode statique symétrique avec deux arguments, MyObject.equals(MyObjecta, MyObject b)
. Cette méthode vérifie ensuite si un ou les deux arguments sont des références nulles. Si je souhaite spécifiquement exclure les références nulles, je crée une méthode supplémentaire, par exemple equalsStrict()
ou similaire, qui effectue une vérification nulle avant de déléguer à l'autre méthode.
Notez que le contrat est "pour toute référence non nulle x". L'implémentation ressemblera donc à:
if (x != null) {
if (x.equals(null)) {
return false;
}
}
x
n'a pas besoin d'être null
pour être considéré comme égal à null
car la définition suivante de equals
est possible:
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;
}
C'est une question délicate. Pour une compatibilité descendante, vous ne pouvez pas le faire.
Imaginez le scénario suivant
void m (Object o) {
if (one.equals (o)) {}
else if (two.equals (o)) {}
else {}
}
Maintenant, avec égal, renvoyer la clause false else sera exécutée, mais pas lors du lancement d'une exception.
De plus, null n'est pas vraiment égal à dire "2", il est donc parfaitement logique de retourner false. Alors il vaut probablement mieux insister null.equals ("b") pour retourner aussi false :))
Mais cette exigence crée une relation d'égalité étrange et non symétrique.