Quelles sont les raisons pour lesquelles Map.get (clé d'objet) n'est pas (entièrement) générique

405

Quelles sont les raisons de la décision de ne pas avoir de méthode get entièrement générique dans l'interface de java.util.Map<K, V>.

Pour clarifier la question, la signature de la méthode est

V get(Object key)

au lieu de

V get(K key)

et je me demande pourquoi (même chose pour remove, containsKey, containsValue).

WMR
la source
3
Question similaire concernant Collection: stackoverflow.com/questions/104799/…
AlikElzin-kilaka
1
Incroyable. J'utilise Java depuis plus de 20 ans, et aujourd'hui je me rends compte de ce problème.
GhostCat

Réponses:

260

Comme mentionné par d'autres, la raison pour laquelle get(), etc. n'est pas générique car la clé de l'entrée que vous récupérez ne doit pas nécessairement être du même type que l'objet auquel vous passez get(); la spécification de la méthode exige seulement qu'elles soient égales. Cela découle de la façon dont la equals()méthode prend un objet en paramètre, et pas seulement du même type que l'objet.

Bien qu'il puisse être généralement vrai que de nombreuses classes se sont equals()définies de sorte que ses objets ne peuvent être égaux qu'aux objets de sa propre classe, il existe de nombreux endroits en Java où ce n'est pas le cas. Par exemple, la spécification de List.equals()indique que deux objets List sont égaux s'ils sont tous deux Lists et ont le même contenu, même s'il s'agit d'implémentations différentes de List. Donc, pour revenir à l'exemple de cette question, selon la spécification de la méthode, il est possible d'avoir un Map<ArrayList, Something>et pour moi d'appeler get()avec un LinkedListargument as, et il devrait récupérer la clé qui est une liste avec le même contenu. Cela ne serait pas possible s'il get()était générique et restreignait son type d'argument.

newacct
la source
28
Alors pourquoi est V Get(K k)en C #?
134
La question est, si vous voulez appeler m.get(linkedList), pourquoi n'avez-vous pas défini mle type de Map<List,Something>? Je ne peux pas penser à un cas d'utilisation où appeler m.get(HappensToBeEqual)sans changer le Maptype pour obtenir une interface est logique.
Elazar Leibovich
58
Wow, grave défaut de conception. Vous n'obtenez aucun avertissement du compilateur non plus, foiré. Je suis d'accord avec Elazar. Si cela est vraiment utile, ce qui, je le doute, arrive souvent, un getByEquals (touche d'objet) semble plus raisonnable ...
mmm
37
Cette décision semble avoir été prise sur la base de la pureté théorique plutôt que pratique. Pour la majorité des usages, les développeurs préféreraient de beaucoup voir l'argument limité par le type de modèle, plutôt que de le laisser illimité pour prendre en charge les cas marginaux comme celui mentionné par newacct dans sa réponse. Laisser les signatures non basées sur des modèles crée plus de problèmes qu'il n'en résout.
Sam Goldberg
14
@newacct: «parfaitement de type sécurisé» est une revendication forte pour une construction qui peut échouer de façon imprévisible au moment de l'exécution. Ne restreignez pas votre vue aux cartes de hachage qui fonctionnent avec cela. TreeMappeut échouer lorsque vous passez des objets du mauvais type à la getméthode mais peut passer occasionnellement, par exemple lorsque la carte est vide. Et pire encore, dans le cas d'un fourni, Comparatorla compareméthode (qui a une signature générique!) Pourrait être appelée avec des arguments du mauvais type sans aucun avertissement non contrôlé. Il s'agit d' un comportement brisé.
Holger
105

Un formidable codeur Java chez Google, Kevin Bourrillion, a écrit à propos de ce problème dans un article de blog il y a quelque temps (certes dans le contexte de Setau lieu de Map). La phrase la plus pertinente:

De manière uniforme, les méthodes du Java Collections Framework (et de la Google Collections Library également) ne restreignent jamais les types de leurs paramètres, sauf lorsque cela est nécessaire pour empêcher la collection de se casser.

Je ne suis pas entièrement sûr d'être d'accord avec cela en tant que principe - .NET semble bien exiger le bon type de clé, par exemple - mais cela vaut la peine de suivre le raisonnement dans le blog. (Après avoir mentionné .NET, il vaut la peine d'expliquer qu'une partie de la raison pour laquelle ce n'est pas un problème dans .NET est qu'il y a le plus gros problème dans .NET de variance plus limitée ...)

Jon Skeet
la source
4
Apocalisp: ce n'est pas vrai, la situation est toujours la même.
Kevin Bourrillion
9
@ user102008 Non, le message n'est pas faux. Même si an Integeret a Doublene peuvent jamais être égaux l'un à l'autre, c'est toujours une bonne question de se demander si a Set<? extends Number>contient la valeur new Integer(5).
Kevin Bourrillion
33
Je n'ai jamais voulu vérifier l'adhésion à un Set<? extends Foo>. J'ai très fréquemment changé le type de clé d'une carte, puis j'ai été frustré que le compilateur ne puisse pas trouver tous les endroits où le code devait être mis à jour. Je ne suis vraiment pas convaincu que ce soit le bon compromis.
Porculus
4
@EarthEngine: Il a toujours été cassé. C'est tout le point - le code est cassé, mais le compilateur ne peut pas l'attraper.
Jon Skeet
1
Et c'est toujours cassé, et nous a juste causé un bug ... une réponse géniale.
GhostCat
28

Le contrat s'exprime ainsi:

Plus formellement, si cette mappe contient un mappage d'une clé k à une valeur v telle que (clé == null? K == null: key.equals (k) ), alors cette méthode renvoie v; sinon, elle renvoie null. (Il peut y avoir au plus un tel mappage.)

(mon accent)

et en tant que tel, une recherche de clé réussie dépend de l'implémentation de la clé d'entrée de la méthode d'égalité. Cela ne dépend pas nécessairement de la classe de k.

Brian Agnew
la source
4
Cela dépend aussi de hashCode(). Sans une implémentation correcte de hashCode (), une bonne implémentation equals()est plutôt inutile dans ce cas.
rudolfson
5
Je suppose que, en principe, cela vous permettrait d'utiliser un proxy léger pour une clé, si la recréation de la clé entière n'était pas pratique - tant que equals () et hashCode () sont correctement implémentés.
Bill Michell
5
@rudolfson: Pour autant que je sache, seul un HashMap dépend du code de hachage pour trouver le bon compartiment. Un TreeMap, par exemple, utilise un arbre de recherche binaire et ne se soucie pas de hashCode ().
Rob
4
A proprement parler, get()n'a pas besoin de prendre un argument de type Objectpour satisfaire le contact. Imaginez que la méthode get soit limitée au type de clé K- le contrat serait toujours valide. Bien sûr, les utilisations où le type de temps de compilation n'était pas une sous-classe de Kne pourraient plus être compilées, mais cela n'invalide pas le contrat, car les contrats discutent implicitement de ce qui se passe si le code compile.
BeeOnRope
16

C'est une application de la loi de Postel, "soyez conservateur dans ce que vous faites, libéral dans ce que vous acceptez des autres".

Des contrôles d'égalité peuvent être effectués quel que soit le type; la equalsméthode est définie sur la Objectclasse et accepte any Objectcomme paramètre. Il est donc logique que l'équivalence des clés et les opérations basées sur l'équivalence des clés acceptent n'importe quel Objecttype.

Lorsqu'une carte renvoie des valeurs clés, elle conserve autant d'informations de type que possible, en utilisant le paramètre type.

erickson
la source
4
Alors pourquoi est V Get(K k)en C #?
1
C'est V Get(K k)en C # parce que ça a aussi du sens. La différence entre les approches Java et .NET est vraiment seulement qui bloque les éléments non correspondants. En C # c'est le compilateur, en Java c'est la collection. Je suis furieux à propos des classes de collection incohérentes de .NET de temps en temps, mais Get()et l' Remove()acceptation d'un type correspondant vous empêche certainement de transmettre accidentellement une mauvaise valeur.
Wormbo
26
C'est une mauvaise application de la loi de Postel. Soyez libéral dans ce que vous acceptez des autres, mais pas trop libéral. Cette API idiote signifie que vous ne pouvez pas faire la différence entre «pas dans la collection» et «vous avez fait une erreur de frappe statique». Plusieurs milliers d'heures de programmation perdues auraient pu être évitées avec get: K -> boolean.
Judge Mental
1
Bien sûr, cela aurait dû l'être contains : K -> boolean.
Judge Mental
4
Postel avait tort .
Alnitak
13

Je pense que cette section du didacticiel sur les génériques explique la situation (je souligne):

"Vous devez vous assurer que l'API générique n'est pas indûment restrictive; elle doit continuer à prendre en charge le contrat d'origine de l'API. Considérez à nouveau quelques exemples de java.util.Collection. L'API pré-générique ressemble à:

interface Collection { 
  public boolean containsAll(Collection c);
  ...
}

Une tentative naïve de le générer est:

interface Collection<E> { 
  public boolean containsAll(Collection<E> c);
  ...
}

Bien que ce type soit certainement sûr, il ne respecte pas le contrat d'origine de l'API. La méthode containsAll () fonctionne avec tout type de collection entrante. Il ne réussira que si la collection entrante ne contient vraiment que des instances de E, mais:

  • Le type statique de la collection entrante peut différer, peut-être parce que l'appelant ne connaît pas le type précis de la collection transmise, ou peut-être parce qu'il s'agit d'une collection <S>, où S est un sous-type de E.
  • Il est parfaitement légitime d'appeler containsAll () avec une collection d'un type différent. La routine devrait fonctionner et retourner fausse. "
Yardena
la source
2
pourquoi pas containsAll( Collection< ? extends E > c )alors?
Judge Mental
1
@JudgeMental, mais pas donné comme exemple ci - dessus , il est également nécessaire de permettre containsAllune Collection<S>Sest un supertype de E. Cela ne serait pas autorisé s'il l'était containsAll( Collection< ? extends E > c ). En outre, comme cela est explicitement indiqué dans l'exemple, il est légitime de passer une collection d'un type différent (avec la valeur de retour étant alors false).
davmac
Il ne devrait pas être nécessaire d'autoriser containsAll avec une collection d'un super-type de E. Je soutiens qu'il est nécessaire de refuser cet appel avec une vérification de type statique pour éviter un bogue. C'est un contrat stupide, qui est, je pense, le point de la question initiale.
Judge Mental
6

La raison en est que le confinement est déterminé par equalset hashCodequelles sont les méthodes activées Objectet les deux prennent un Objectparamètre. Il s'agissait d'un premier défaut de conception dans les bibliothèques standard de Java. Couplé aux limitations du système de type Java, il oblige tout ce qui s'appuie sur equals et hashCode à prendre Object.

La seule façon d'avoir des tables de hachage typées et l' égalité en Java est à éviter Object.equalset Object.hashCodeet utiliser un substitut générique. Java fonctionnel est fourni avec des classes de type dans ce but: Hash<A>et Equal<A>. Un wrapper pour HashMap<K, V>est fourni qui prend Hash<K>et Equal<K>dans son constructeur. Ces classes getet containsméthodes prennent donc un argument générique de type K.

Exemple:

HashMap<String, Integer> h =
  new HashMap<String, Integer>(Equal.stringEqual, Hash.stringHash);

h.add("one", 1);

h.get("one"); // All good

h.get(Integer.valueOf(1)); // Compiler error
Apocalisp
la source
4
Cela en soi n'empêche pas le type de 'get' d'être déclaré comme "V get (clé K)", car 'Object' est toujours un ancêtre de K, donc "key.hashCode ()" serait toujours valide.
finnw
1
Bien que cela ne l'empêche pas, je pense que cela l'explique. S'ils ont changé la méthode equals pour forcer l'égalité des classes, ils ne pourraient certainement pas dire aux gens que le mécanisme sous-jacent pour localiser l'objet dans la carte utilise equals () et hashmap () lorsque les prototypes de méthode pour ces méthodes ne sont pas compatibles.
cgp
5

Compatibilité.

Avant que les génériques ne soient disponibles, il y avait juste get (Object o).

S'ils avaient changé cette méthode pour obtenir (<K> o), cela aurait potentiellement forcé une maintenance massive du code sur les utilisateurs java juste pour que le code de travail soit à nouveau compilé.

Ils auraient pu introduire une méthode supplémentaire , par exemple get_checked (<K> o) et déconseiller l'ancienne méthode get () afin qu'il y ait un chemin de transition plus doux. Mais pour une raison quelconque, cela n'a pas été fait. (La situation dans laquelle nous nous trouvons actuellement est que vous devez installer des outils tels que findBugs pour vérifier la compatibilité des types entre l'argument get () et le type de clé déclaré <K> de la carte.)

Les arguments relatifs à la sémantique de .equals () sont faux, je pense. (Techniquement, ils sont corrects, mais je pense toujours qu'ils sont faux. Aucun concepteur sensé ne rendra jamais o1.equals (o2) vrai si o1 et o2 n'ont pas de superclasse commune.)

Erwin Smout
la source
4

Il y a une raison de plus, cela ne peut pas être fait techniquement, car cela casse la carte.

Java a une construction générique polymorphe comme <? extends SomeClass>. Une telle référence marquée peut pointer vers un type signé avec <AnySubclassOfSomeClass>. Mais générique polymorphe rend cette référence en lecture seule . Le compilateur vous permet d'utiliser des types génériques uniquement comme type de méthode de retour (comme les simples getters), mais bloque l'utilisation de méthodes où le type générique est un argument (comme les setters ordinaires). Cela signifie que si vous écrivez Map<? extends KeyType, ValueType>, le compilateur ne vous permet pas d'appeler la méthode get(<? extends KeyType>)et la carte sera inutile. La seule solution est de rendre cette méthode pas générique: get(Object).

Owheee
la source
pourquoi alors la méthode set est-elle fortement typée?
Sentenza
si vous voulez dire 'put': La méthode put () change de carte et elle ne sera pas disponible avec des génériques comme <? étend SomeClass>. Si vous l'appelez, vous avez obtenu une exception de compilation. Une telle carte sera "en lecture seule"
Owheee
1

Rétrocompatibilité, je suppose. Map(ou HashMap) doit encore prendre en charge get(Object).

Anton Gogolev
la source
13
Mais le même argument pourrait être avancé put(ce qui restreint les types génériques). Vous obtenez une compatibilité descendante en utilisant des types bruts. Les génériques sont «opt-in».
Thilo
Personnellement, je pense que la raison la plus probable de cette décision de conception est la compatibilité descendante.
geekdenz
1

Je regardais cela et je me demandais pourquoi ils l'avaient fait de cette façon. Je ne pense pas que l'une des réponses existantes explique pourquoi ils ne pouvaient pas simplement faire en sorte que la nouvelle interface générique n'accepte que le type approprié pour la clé. La vraie raison est que même s'ils ont introduit des génériques, ils n'ont PAS créé une nouvelle interface. L'interface de la carte est la même ancienne carte non générique qu'elle sert simplement de version générique et non générique. De cette façon, si vous avez une méthode qui accepte une carte non générique, vous pouvez la transmettre Map<String, Customer>et cela fonctionnerait toujours. Dans le même temps, le contrat pour get accepte Object, la nouvelle interface devrait donc également prendre en charge ce contrat.

À mon avis, ils auraient dû ajouter une nouvelle interface et les implémenter à la fois sur la collection existante, mais ils ont décidé en faveur d'interfaces compatibles même si cela signifie une conception pire pour la méthode get. Notez que les collections elles-mêmes seraient compatibles avec les méthodes existantes, seules les interfaces ne le seraient pas.

Stilgar
la source
0

Nous effectuons un gros refactoring en ce moment et nous manquions ce get () fortement typé pour vérifier que nous n'avons pas manqué un get () avec l'ancien type.

Mais j'ai trouvé une astuce de contournement / laid pour la vérification du temps de compilation: créer une interface de carte avec get fortement tapé, containsKey, supprimer ... et le mettre dans le package java.util de votre projet.

Vous obtiendrez des erreurs de compilation juste en appelant get (), ... avec des types incorrects, tout le reste semble correct pour le compilateur (au moins à l'intérieur de l'éclipse kepler).

N'oubliez pas de supprimer cette interface après vérification de votre build car ce n'est pas ce que vous voulez en runtime.

Henva
la source