web-dev-qa-db-fra.com

Est-ce une mauvaise idée si equals (null) lève NullPointerException à la place?

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) devrait return 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:

  • Pourquoi est-ce une bonne idée que o1.equals(o2) devrait return false Au lieu de lancer NullPointerException?
  • Serait-ce une mauvaise idée si, dans la mesure du possible, nous réécrivons le contrat pour que anyObject.equals(null) lance toujours NullPointerException à la place?

En comparaison avec Comparable

En revanche, c'est ce que dit le Comparable contrat :

Notez que null n'est une instance d'aucune classe, et e.compareTo(null) doit lancer un NullPointerException même si e.equals(null) renvoie false.

Si NullPointerException est approprié pour compareTo, pourquoi ne l'est-il pas pour equals?

Questions connexes


Un argument purement sémantique

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?

Objets JLS 4.3.1

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 test
  • Par conséquent, equals(null) devrait lancer NullPointerException
75

À 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:

  • Demandez à n'importe quel homme s'il est aussi bon que l'homme suivant et chacun dira oui.
  • Demandez à n'importe quel homme s'il est aussi bon que personne et chacun dira non.
  • Ne demandez à personne si c'est aussi bon qu'un homme et vous n'obtiendrez jamais de réponse.

À ce moment, le compilateur a atteint Enlightenment.

102
Sean Owen

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.

19
duffymo

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
   ... 
}
8
Angus

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.

4
DaveJohnston

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. ..

4
arg20

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.

2
Rob L

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.

2
fastcodejava

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.

2
Arkku

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.

1
mdma

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;
}
1
Vijay Mathew

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.

0
Anton