Objets mutables et hashCode

Avoir la classe suivante:

public class Member { private int x; private long y; private double d; public Member(int x, long y, double d) { this.x = x; this.y = y; this.d = d; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + x; result = (int) (prime * result + y); result = (int) (prime * result + Double.doubleToLongBits(d)); return result; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj instanceof Member) { Member other = (Member) obj; return other.x == x && other.y == y && Double.compare(d, other.d) == 0; } return false; } public static void main(Ssortingng[] args) { Set test = new HashSet(); Member b = new Member(1, 2, 3); test.add(b); System.out.println(b.hashCode()); bx = 0; System.out.println(b.hashCode()); Member first = test.iterator().next(); System.out.println(test.contains(first)); System.out.println(b.equals(first)); System.out.println(test.add(first)); } 

}

Il produit les résultats suivants:
30814 29853 false true true

Étant donné que le hashCode dépend de l’état de l’object qu’il ne peut plus être récupéré correctement, la vérification du confinement échoue. Le HashSet ne fonctionne plus correctement. Une solution serait de rendre les membres immuables, mais est-ce la seule solution? Toutes les classes ajoutées à HashSets doivent-elles être immuables? Y a-t-il un autre moyen de gérer la situation?

Cordialement.

Les objects dans les hashsets doivent être immuables ou faire preuve de discipline pour ne pas les modifier après leur utilisation dans un hashset (ou hashmap).

Dans la pratique, j’ai rarement constaté que c’était un problème – je me trouve rarement obligé d’utiliser des objects complexes car les clés sont des éléments définis et quand je le fais, ce n’est généralement pas un problème que de ne pas les muter. Bien sûr, si vous avez déjà exposé les références à un autre code, cela peut devenir plus difficile.

Oui. Tout en maintenant votre classe mutable, vous pouvez calculer les méthodes hashCode et equals en fonction des valeurs immuables de la classe (peut-être un identifiant généré) afin de respecter le contrat hashCode défini dans la classe Object:

  • Chaque fois qu’elle est appelée plusieurs fois sur le même object lors de l’exécution d’une application Java, la méthode hashCode doit systématiquement renvoyer le même entier, à condition qu’aucune information utilisée dans les comparaisons égales de l’object ne soit modifiée. Cet entier ne doit pas nécessairement restr cohérent d’une exécution d’une application à une autre exécution de la même application.

  • Si deux objects sont égaux selon la méthode equals (Object), alors l’appel de la méthode hashCode sur chacun des deux objects doit produire le même résultat entier.

  • Il n’est pas nécessaire que si deux objects ne soient pas égaux en fonction de la méthode equals (java.lang.Object), l’appel de la méthode hashCode sur chacun des deux objects doit produire des résultats entiers distincts. Cependant, le programmeur doit savoir que la production de résultats entiers distincts pour des objects inégaux peut améliorer les performances des hashtables.

Selon votre situation, cela peut être plus facile ou non.

 class Member { private static long id = 0; private long id = Member.id++; // other members here... public int hashCode() { return this.id; } public boolean equals( Object o ) { if( this == o ) { return true; } if( o instanceOf Member ) { return this.id == ((Member)o).id; } return false; } ... } 

Si vous avez besoin d’un atsortingbut thread-safe, vous pouvez utiliser plutôt plutôt AtomicLong , mais là encore, cela dépend de la manière dont vous allez utiliser votre object.

Jon Skeet a énuméré toutes les alternatives. En ce qui concerne pourquoi les clés dans une carte ou un ensemble ne doivent pas changer:

Le contrat d’un ensemble implique qu’à tout moment, il n’y a pas deux objects o1 et o2 tels que

 o1 != o2 && set.contains(o1) && set.contains(o2) && o1.equals(o2) 

La raison pour laquelle cela est nécessaire est particulièrement claire pour une carte. Du contrat de Map.get ():

Plus formellement, si cette carte contient une correspondance entre une clé k et une valeur v telle que (key==null ? k==null : key.equals(k)) , cette méthode renvoie v , sinon elle renvoie null . (Il peut y avoir au plus un tel mapping.)

Maintenant, si vous modifiez une clé insérée dans une carte, vous pouvez la rendre égale à une autre clé déjà insérée. De plus, la carte ne peut pas savoir que vous l’avez fait. Alors, que doit faire la carte si vous faites ensuite map.get(key) , key correspondant à plusieurs clés de la carte? Il n’existe aucun moyen intuitif de définir ce que cela signifierait – principalement parce que notre intuition pour ces types de données est l’idéal mathématique des ensembles et des mappages, qui n’ont pas à traiter avec des clés changeantes, car leurs clés sont des objects mathématiques et donc immuables.

Comme déjà mentionné, on peut accepter les trois solutions suivantes:

  1. Utilisez des objects immuables; Même lorsque votre classe est mutable, vous pouvez utiliser des identités immuables sur votre implémentation de hashcode et vérifier, par exemple, une valeur de type ID.
  2. De même que ci-dessus, implémentez add / remove pour obtenir un clone de l’object inséré, pas la référence réelle. HashSet n’offre pas de fonction get (par exemple pour vous permettre de modifier l’object ultérieurement); ainsi, vous êtes en sécurité, il n’y aura pas de doublons.
  3. Faites preuve de discipline en ne les changeant pas après leur utilisation, comme le suggère @ Jon Skeet

Mais si, pour une raison quelconque, vous avez vraiment besoin de modifier des objects après avoir été insérés dans un HashSet , vous devez trouver un moyen «d’informer» votre collection des nouveaux changements. Pour réaliser cette fonctionnalité:

  1. Vous pouvez utiliser le modèle de conception Observer et étendre HashSet pour implémenter l’interface Observer . Les objects de votre Member doivent être Observable et update le jeu de HashSet sur tout programme de HashSet ou autre méthode qui affecte le hashcode et / ou equals .

Remarque 1: Extension de 3, utilisation de 4: nous pouvons accepter les modifications, mais celles qui ne créent pas d’object déjà existant (par exemple, j’ai mis à jour un ID d’utilisateur, en lui atsortingbuant un nouvel ID mais en ne le définissant pas avec un object existant). Sinon, vous devez tenir compte du scénario dans lequel un object est transformé de manière à être désormais égal à un autre object existant dans l’ Set . Si vous acceptez cette limitation, la 4ème suggestion fonctionnera correctement, sinon vous devez être proactif et définir une stratégie pour de tels cas.

Remarque 2: Vous devez fournir les états précédent et actuel de l’object modifié lors de la update œuvre de la update , car vous devez initialement supprimer l’élément le plus ancien (par exemple, utilisez getClone() avant de définir de nouvelles valeurs), puis ajoutez l’object avec le nouvel état. . L’extrait suivant n’est qu’un exemple d’implémentation. Il nécessite des modifications en fonction de votre stratégie d’ajout de doublon.

 @Override public void update(Observable newItem, Object oldItem) { remove(oldItem); if (add(newItem)) newItem.addObserver(this); } 

J’ai utilisé des techniques similaires sur des projets, pour lesquels j’ai besoin de plusieurs index sur une classe, afin de pouvoir rechercher avec O (1) des ensembles d’objects partageant une identité commune. Imaginez-le en tant que MultiKeymap de HashSets (ceci est très utile, car vous pouvez alors recouper / indexer des index et travailler de manière similaire à une recherche de type SQL). Dans de tels cas, j’annote les méthodes (généralement les setters) qui doivent déclencher Change-update chacun des index lorsqu’un changement important se produit, afin que les index soient toujours mis à jour avec les derniers états.

Théoriquement (et le plus souvent pas pratiquement) votre classe soit:

  1. a une identité naturelle immuable qui peut être déduite d’un sous-ensemble de ses champs, auquel cas vous pouvez utiliser ces champs pour générer le hashCode .
  2. n’a pas d’identité naturelle, auquel cas il est inutile d’utiliser un Set pour les stocker; vous pouvez également utiliser une List .

Ne changez jamais le champ “hashable” après avoir inséré un conteneur de hachage.

Comme si vous (membre) aviez enregistré votre numéro de téléphone (membre.x) dans une page jaune (conteneur basé sur un hachage), mais que vous aviez changé de numéro, personne ne peut plus vous trouver dans la page jaune.