web-dev-qa-db-fra.com

Faut-il autoriser l'ajout d'un hachage à Java?

Selon le contrat d'un ensemble en Java, "il n'est pas autorisé pour un ensemble de se contenir lui-même en tant qu'élément" ( source ). Toutefois, cela est possible dans le cas d'un hachage d'objets, comme illustré ici:

Set<Object> mySet = new HashSet<>();
mySet.add(mySet);
assertThat(mySet.size(), equalTo(1));

Cette assertion est acceptable, mais je suppose que le comportement sera soit que l'ensemble résultant soit égal à 0, soit qu'il lise une exception. Je me rends compte que l'implémentation sous-jacente d'un HashSet est un HashMap, mais il semble qu'il devrait y avoir un contrôle d'égalité avant d'ajouter un élément pour éviter de violer ce contrat, non?

52
davidmerrick

D'autres ont déjà expliqué pourquoi il est discutable d'un point de vue mathématique, en se référant à le paradoxe de Russell .

Cela ne répond pas à votre question sur un niveau technique, cependant.

Alors disséquons ceci:

Tout d’abord, une fois de plus la partie pertinente de la JavaDoc de l’interface Set:

Remarque: vous devez faire très attention si des objets mutables sont utilisés comme éléments d'ensemble. Le comportement d'un ensemble n'est pas spécifié si la valeur d'un objet est modifiée de manière à affecter les comparaisons égales alors que l'objet est un élément de l'ensemble. Un cas particulier de cette interdiction est qu’il n’est pas permis à un ensemble de se contenir en tant qu’élément.

Fait intéressant, le JavaDoc de l'interface List présente un énoncé similaire, bien que légèrement plus faible, et plus technique:

Bien qu'il soit permis aux listes de se contenir en tant qu'éléments, une extrême prudence est conseillée: les méthodes equals et hashCode ne sont plus bien définies sur une telle liste.

Et enfin, le noeud est dans le JavaDoc de l'interface Collection , qui est l'ancêtre commun des interfaces Set et List:

Certaines opérations de collection qui effectuent une traversée récursive de la collection peuvent échouer, à l'exception des instances auto-référentielles dans lesquelles la collection se contient directement ou indirectement . Cela inclut les méthodes clone(), equals(), hashCode() et toString(). Les implémentations peuvent éventuellement gérer le scénario auto-référentiel, mais la plupart des implémentations actuelles ne le font pas.

(Souligné par moi)

La partie audacieuse indique pourquoi l’approche que vous avez proposée dans votre question ne serait pas suffisante:

il semble qu'il devrait y avoir un contrôle d'égalité avant d'ajouter un élément pour éviter de violer ce contrat, non?

Cela ne vous aiderait pas ici. Le point clé est que vous rencontrerez toujours des problèmes lorsque la collection se contiendra directement ou indirectement . Imaginez ce scénario:

Set<Object> setA = new HashSet<Object>();
Set<Object> setB = new HashSet<Object>();
setA.add(setB);
setB.add(setA);

Évidemment, aucun des ensembles ne contient lui-même directement. Mais chacune d’elles contient l’autre - et donc elle-même indirectement. Cela ne pourrait pas être évité par un simple contrôle d’égalité référentielle (en utilisant == dans la méthode add).


Éviter un tel "état incohérent" est fondamentalement impossible dans la pratique. Bien sûr, il est possible en théorie d’utiliser des calculs référentiels Accessibilité . En fait, le ramasse-miettes doit faire exactement cela!

Mais cela devient impossible en pratique lorsque des classes personnalisées sont impliquées. Imaginez un cours comme celui-ci:

class Container {

    Set<Object> set;

    @Override 
    int hashCode() {
        return set.hashCode(); 
    }
}

Et déconner avec ceci et son set:

Set<Object> set = new HashSet<Object>();
Container container = new Container();
container.set = set;
set.add(container);

La méthode add de Set n'a fondamentalement aucun moyen de détecter si l'objet qui y est ajouté a une référence une (indirecte) à l'ensemble lui-même.

Longue histoire courte:

Vous ne pouvez pas empêcher le programmeur de tout gâcher.

52
Marco13

L'ajout de la collection en elle-même ne fois entraîne le test. L'ajouter deux fois provoque le StackOverflowError que vous cherchiez.

En tant que développeur personnel, il n’a aucun sens de faire appliquer une vérification dans le code sous-jacent pour empêcher cela. Le fait que vous obteniez un StackOverflowError dans votre code si vous tentez de le faire trop souvent, ou si vous calculez le hashCode - ce qui provoquerait un débordement instantané - devrait suffire à éviter toute perte de sens. développeur garderait ce genre de code dans leur base de code.

22
Makoto

Vous devez lire le document complet et le citer complètement:

Le comportement d'un ensemble n'est pas spécifié si la valeur d'un objet est modifiée de manière à affecter les comparaisons égales alors que l'objet est un élément de l'ensemble. Un cas spécial de cette interdiction est qu’il n’est pas permis à un ensemble de se contenir lui-même en tant qu’élément.

La restriction actuelle est dans la première phrase. Le comportement est non spécifié si un élément d'un ensemble est muté.

Étant donné que l'ajout d'un ensemble à lui-même le transforme et que l'ajouter à nouveau le transforme à nouveau, le résultat est indéterminé.

Notez que la restriction est que le comportement est non spécifié et qu’un cas spécial de cette restriction ajoute l'ensemble à lui-même.

La doc dit donc, en d'autres termes, que l'ajout d'un ensemble à lui-même entraîne un comportement non spécifié, ce que vous voyez. C'est à l'implémentation concrète de traiter (ou non).

12
Polygnome

Je conviens avec vous que d’un point de vue mathématique, ce comportement n’a vraiment aucun sens.

Il y a deux questions intéressantes ici: premièrement, dans quelle mesure les concepteurs de l'interface Set ont-ils essayé d'implémenter un ensemble mathématique? Deuxièmement, même s'ils n'étaient pas, dans quelle mesure cela les exempte des règles de la théorie des ensembles?

Pour la première question, je vous indiquerai la documentation du Set:

Une collection qui ne contient aucun élément en double. Plus formellement, les ensembles ne contiennent pas de paire d'éléments e1 et e2 tels que e1.equals (e2) et au plus un élément null. Comme son nom l'indique, cette interface modélise l'abstraction de l'ensemble mathématique.

Il convient de mentionner ici que les formulations actuelles de la théorie des ensembles ne permettent pas aux ensembles d'être membres d'eux-mêmes. (Voir le Axiome de régularité ). Ceci est dû en partie à le paradoxe de Russell , qui a révélé une contradiction dans théorie des ensembles naïve (qui permettait à un ensemble d'être n'importe quel collection de objets - il n'y avait aucune interdiction contre les ensembles comprenant eux-mêmes). Ceci est souvent illustré par le Barber Paradox : supposons que, dans une ville donnée, un coiffeur rase tous les hommes - et seulement les hommes - qui ne rasent pas se. Question: le coiffeur se rase-t-il? S'il le fait, cela viole la deuxième contrainte; s'il ne le fait pas, cela viole la première contrainte. C’est logiquement impossible, mais c’est en fait parfaitement acceptable selon les règles de la théorie des ensembles naïve (c’est pourquoi la nouvelle formulation "standard" de la théorie des ensembles interdit explicitement aux ensembles de se contenir).

Il y a plus de discussion dans cette question sur Math.SE sur la raison pour laquelle les ensembles ne peuvent pas être un élément d'eux-mêmes.

Cela dit, cela soulève la deuxième question: même si les concepteurs n'avaient pas essayaient explicitement de modéliser un ensemble mathématique, cela serait-il totalement "exempté" des problèmes associés au jeu naïf? théorie? Je ne pense pas - je pense que beaucoup des problèmes qui posaient problème à la théorie des ensembles naïve seraient un fléau n'importe lequel d'une sorte de collection insuffisamment limitée, d'une manière analogue à la théorie des ensembles naïve. Il se peut que je lise trop, mais la première partie de la définition de Set dans la documentation ressemble étrangement au concept intuitif d’un ensemble dans la théorie des ensembles naïve:

Une collection qui ne contient aucun élément en double.

Certes (et à leur crédit), ils placent au moins certains contraintes plus tard (notamment en déclarant que vous ne devriez vraiment pas essayer de contenir un ensemble), mais vous pouvez vous demander si c'est vraiment "assez" pour éviter les problèmes avec la théorie des ensembles naïve. C'est pourquoi, par exemple, vous rencontrez un problème de "tortues à fond" lorsque vous essayez de calculer le code de hachage d'un HashSet contenant lui-même. Comme certains l’ont suggéré, ce n’est pas simplement un problème pratique, c’est une illustration des problèmes théoriques fondamentaux de ce type de formulation.

Pour faire une brève digression, je reconnais qu'il existe, bien sûr, certaines limites quant au degré de précision avec lequel une classe de collection peut réellement modéliser un ensemble mathématique. Par exemple, la documentation de Java met en garde contre les dangers d'inclure des objets mutables dans un ensemble. Certains autres langages, tels que Python, tentent au moins de bannir entièrement de nombreux types d'objets mutables :

Les classes définies sont implémentées à l'aide de dictionnaires. En conséquence, les exigences pour les éléments de jeu sont les mêmes que pour les clés de dictionnaire; à savoir que l'élément définit à la fois __eq__() et __hash__(). Par conséquent, les ensembles ne peuvent pas contenir d'éléments mutables tels que des listes ou des dictionnaires. Cependant, ils peuvent contenir des collections immuables telles que des n-uplets ou des instances de ImmutableSet. Pour faciliter la mise en œuvre des ensembles d'ensembles, les ensembles internes sont automatiquement convertis en une forme immuable. Par exemple, Set([Set(['dog'])]) est transformé en Set([ImmutableSet(['dog'])]).

Deux autres différences majeures, soulignées par d’autres, sont

  • Les jeux Java sont mutables
  • Les ensembles Java sont finis. Évidemment, cela sera vrai de any classe de collection: en dehors des préoccupations concernant l'infini actuel , les ordinateurs ne disposent que d'une quantité de mémoire finie. (Certaines langues, comme Haskell, ont des structures de données infinies paresseuses; cependant, à mon avis, une séquence de choix semblable à la loi semble être une façon plus naturelle de les modéliser que la théorie des ensembles classique, mais ce n'est que mon opinion).

TL; DR Non, cela ne devrait vraiment pas être autorisé (ou du moins, vous ne devriez jamais le faire) car les ensembles ne peuvent pas être membres de se.

8
EJoshuaS