Vous aimez ce que vous lisez sur ce blog ?
Envie d'aller plus loin avec véritable formation d'expertise en Java ?
Venez suivre ma formation Masterclasse Expertise Java !

"Même un développeur experimenté a besoin de continuer à apprendre. Et dans cette formation... j'ai appris beaucoup !" - A.G., Java Champion

Sessions intra-entreprises sur demande : contact[at]mokatech.net.
Inscrivez-vous vite !

Java Quiz #37

Voici un nouveau quiz, pour bien finir le mois de mai avant d'entamer ce beau mois de juin.

La classe ColoredPoint ci-dessous pose un problème assez subtil. Lequel ?

  1. public class Point {
  2. private int x;
  3. private int y;
  4.  
  5. public Point(int x, int y) {
  6. this.x = x;
  7. this.y = y;
  8. }
  9.  
  10. public boolean equals(Object o) {
  11. if (this == o) { return true; }
  12. if (!(o instanceof Point)) { return false; }
  13.  
  14. final Point point = (Point) o;
  15. return (x == point.x && y == point.y);
  16. }
  17.  
  18. public int hashCode() {
  19. int result = x;
  20. result = 31 * result + y;
  21. return result;
  22. }
  23. }
  24.  
  25. public class ColoredPoint extends Point {
  26. private Color color;
  27.  
  28. public ColoredPoint(int x, int y, Color color) {
  29. super(x, y);
  30. this.color = color;
  31. }
  32.  
  33. public boolean equals(Object o) {
  34. if (this == o) { return true; }
  35. if (!(o instanceof ColoredPoint && super.equals(o))) {
  36. return false;
  37. }
  38.  
  39. final ColoredPoint point = (ColoredPoint) o;
  40. return (color == null ? (point.color == null) : color.equals(point.color));
  41. }
  42.  
  43. public int hashCode() {
  44. int result = super.hashCode();
  45. result = 31 * result + (color != null ? color.hashCode() : 0);
  46. return result;
  47. }
  48. }

Je suis désolé de décevoir Alexis, mais il est toujours bon de revenir de temps en temps sur ses classiques (et désolé aussi que le code du quiz soit aussi proche de celui du livre de J. Bloch : n'ayant pas ce livre dans ma bibliothèque, je ne m’en suis rendu compte qu’après coup.)

En effet, comme un certain nombre d’entre vous l’avaient vu (mais pas tous, semble-t-il), le problème vient de l’implémentation de Point.equals(). Notez que c'est ColoredPoint.equals() qui pose un problème, mais que c'est Point.equals() qui le crée (par contre, Point.hashCode() et ColoredPoint.hashCode() ne posent pas de problème particulier).

Si l’on revient à sa spécification, qui en détaille le contrat, la méthode Object.equals() doit être symétrique : quelles que soient les références x et y non-null, x.equals(y) doit retourner true si et seulement si y.equals(x) retourne true. Ce qui, comme l’a bien montré Jcs dans son commentaire, n’est pas le cas ici dès lors que l’on compare un Point et un ColoredPoint de mêmes coordonnées.

Au-delà de son caractère un peu théorique, est-ce si grave en pratique de violer cette règle de la symétrie ? Réponse : oui.

Joint à ce billet, vous trouverez une petite classe Java (TestEqualsWithSubclass) qui met clairement en évidence le problème que pose un equals() non symétrique. Elle reprend les deux classes Point et ColoredPoint ci-dessus. Elle définit également une petite classe ArraySet toute bête qui n’est rien d’autre qu’un Set reposant sur un ArrayList (et dont l’implémentation, bien que courte, est correcte et suffisante !)

Et le main de cette classe montre que même si on ajoute les mêmes objets à deux implémentations de Set différentes, l’un des Set peut prétendre avoir un contenu différent de l’autre ! (La méthode contains(), appelée avec le même objet, pouvant retourner un résultat différent selon l’implémentation de Set choisie.)

Ce genre de bug est assez difficile à débusquer. Par exemple, dans la classe jointe à ce billet, la plupart des développeurs vont commencer par penser que le bug se trouve dans l’implémentation de Set, sans imaginer une seconde qu'il puisse en fait se trouver dans une des classes dont on a placé des instances dans ce Set. C’est d’autant plus gênant que le code produit par certains générateurs de code d’IDE contient cette erreur.

Dernière question : comment corriger cette erreur ? Réponse : dans le code du quiz, on peut remplacer chacun des deux tests "o instanceof Point" et "o instanceof ColoredPoint" par "this.getClass() == o.getClass()". (En fait, ici, ne remplacer que le test sur Point suffit : preuve que le problème est bien créé par la classe Point et non ColoredPoint. Néanmoins, on retrouverait le même problème avec ColoredPoint si on la sous-classait.)

La solution semble bonne, mais un autre problème survient alors : un ColoredPoint n’est plus tout à fait une sorte de Point, puisque Point est une classe concrète, et qu'on a garanti qu'un Point et un ColoredPoint ne pouvaient être égaux.

Autrement dit, on ne peut pas éviter le problème, à moins de violer un des principes fondamentaux de l’objet, l’héritage.


Commentaires

1. Le lundi 31 mai 2010, 09:35 par Jcs

Je pense qu'il y a un problème dans le equals qui est censé définir une relation d'équivalence. Ici si j'ai 2 points :

Point p1 = new Point(1,2);
Point p2 = new ColoredPoint(1,2,Color.GREEN);

p2.equals(p1) est false car p1 n'est pas une instance de ColoredPoint

en revanche

p1.equals(p2) est true car les 2 points partagent les même corrdonées.

2. Le lundi 31 mai 2010, 14:44 par Colin HEBERT

Maintenant ce qui est interessant c'est de voir les méthodes des uns et des autres pour corriger ce problème là.

3. Le lundi 31 mai 2010, 14:52 par Alexis

Mouaih, je me méfie de ta solution, Jcs. Olivier nous a habitué à plus fin que l'application d'un item de "Effective Java" de Joshua Bloch... Je crois même que l'exemple ressemble beaucoup à celui du livre.
Je crains qu'il ne faille chercher plus loin.

4. Le mardi 1 juin 2010, 10:08 par Thomas

Pour moi il y a un problème dans ColoredPoint.hashCode().
Le hashCode de Color étant toujours un entier négatif, il doit être possible de trouver des valeurs de x, y et color telles que:
31(31x + y) = -(color.hashCode())

5. Le mardi 1 juin 2010, 11:28 par Benoit BESNARD

Pour la solution de Jcs, je pense que le résultat est normal car, comme tu l'as dit, l'instance p1 est un objet Point mais en aucun cas un objet ColoredPoint. Par contre, p2 étant un Point par héritage, seul l'appel de la méthode equals(Object o) de la classe Point doit retourner vrai.

Pour la réflexion de Thomas, cela signifierait que le hashCode peut-être nul et ce dans différents. Il serait peut-être intéressant de voir plusieurs hashCode de Color pour voir si, effectivement, il est réellement possible avec des coordonnées raisonnables de récupérer un hashCode nul.

P.S. : salut Colin ;)

6. Le mardi 1 juin 2010, 13:01 par Brice

En fait le truc c'est que si deux objet sont égaux, ils doivent avoir le même hashCode!

Ainsi suivant l'exemple de JCS :
Point p1 = new Point(1,2);
Point p2 = new ColoredPoint(1,2,Color.GREEN);
p1.equals(p2); // true
p1.hashCode() == p2.hashCode(); // false !

Donc ce code ne respecte pas le contrat de hashCode. A noter quand même que deux hashCode peuvent-être identique même si deux objets ne sont pas égal.

7. Le vendredi 4 juin 2010, 14:10 par HollyDays

@ Thomas

Le contrat de hashCode() ne spécifie pas que cette méthode doive retourner un résultat non nul.
Ainsi, Color.hashCode() elle-même peut retourner 0 : par exemple, pour les points new Point(0, 0), new Point(-1, 31) ou new Point(1, -31).

8. Le lundi 14 juin 2010, 07:53 par wolfi

les grands esprits se rencontrent!
http://www.demos.fr/espace-metier/i...

Ajouter un commentaire

Le code HTML est affiché comme du texte et les adresses web sont automatiquement transformées.