Quelles sont les raisons derrière 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
).
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 que vous transmettez à get()
; la spécification de la méthode exige seulement qu'elles soient égales. Cela découle de la manière dont la méthode equals()
prend un objet en paramètre, et pas seulement le même type que l'objet.
Bien qu'il soit généralement vrai que de nombreuses classes ont défini equals()
de sorte que ses objets ne puissent être égaux qu'aux objets de sa propre classe, il existe de nombreux endroits dans Java où Par exemple, la spécification de List.equals()
indique que deux objets de liste sont égaux s'ils sont à la fois de même liste 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 LinkedList
comme argument , et il devrait récupérer la clé qui est une liste avec le même contenu, ce qui ne serait pas possible si get()
était générique et restreignait son type d’argument.
Un génial Java coder chez Google, Kevin Bourrillion, a écrit à propos de cette question dans un message de blog il y a quelque temps (certes dans le contexte de Set
au lieu de Map
). La phrase la plus pertinente:
Les méthodes de Java Collections Framework (et de la bibliothèque Google Collections) également) ne restreignent jamais les types de leurs paramètres, sauf dans les cas où il est nécessaire d'empêcher que la collection ne soit endommagée.
Je ne suis pas tout à fait sûr de l'accepter en tant que principe - par exemple, .NET semble être correct, mais il convient de suivre le raisonnement présenté dans l'article du blog. (Ayant mentionné .NET, cela vaut la peine d’expliquer pourquoi ce n’est pas un problème dans .NET, c’est qu’il existe un problème plus important dans .NET de variance plus limitée ...)
Le contrat est exprimé ainsi:
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 retourne v ; sinon, il retourne null. (Il peut y avoir au plus un tel mappage.)
(mon emphase)
et en tant que tel, une recherche de clé réussie dépend de la mise en œuvre par la clé d'entrée de la méthode d'égalité. Ce n'est pas nécessairement dépendant de la classe de k.
C'est une application de loi de Postel, "soyez conservateur dans ce que vous faites, soyez libéral dans ce que vous acceptez des autres".
Les contrôles d'égalité peuvent être effectués indépendamment du type. la méthode equals
est définie sur la classe Object
et accepte n'importe quel paramètre Object
. Il est donc logique pour l'équivalence de clé et les opérations basées sur l'équivalence de clé d'accepter n'importe quel type Object
.
Lorsqu'une carte renvoie des valeurs de clé, elle conserve autant d'informations de type que possible en utilisant le paramètre type.
Je pense que cette section de Generics Tutorial explique la situation (mon emphase):
"Vous devez vous assurer que l'API générique n'est pas excessivement restrictive; elle doit continuer à prendre en charge le contrat d'origine de l'API. Reprenons quelques exemples tirés de Java.util.Collection. L'API pré-générique ressemble à ceci:
interface Collection {
public boolean containsAll(Collection c);
...
}
Une tentative naïve de le généraliser est:
interface Collection<E> {
public boolean containsAll(Collection<E> c);
...
}
Bien que ce soit certainement sûr, cela ne correspond pas au contrat original de l’API. La méthode containsAll () fonctionne avec n’importe quel type de collecte entrante. Cela ne réussira que si la collection entrante contient réellement uniquement des instances de E, mais:
La raison en est que le confinement est déterminé par equals
et hashCode
qui sont des méthodes sur Object
et que les deux prennent un paramètre Object
. C'était un défaut de conception précoce dans les bibliothèques standard de Java. Couplé aux limitations du système de types de Java, il force tout ce qui repose sur equals et hashCode à prendre Object
.
Le seul moyen d’avoir des tables de hachage et une égalité sécurisées pour le type dans Java est d’éviter Object.equals
et Object.hashCode
et utilisez un substitut générique. Java fonctionnel est fourni avec des classes de types uniquement à cette fin: Hash<A>
et Equal<A>
. Un wrapper pour HashMap<K, V>
est fourni qui prend Hash<K>
et Equal<K>
dans son constructeur. Les méthodes get
et contains
de cette classe 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
Il y a une raison de plus, cela ne peut pas être fait techniquement, parce que ça casse la carte.
Java a une construction générique polymorphe comme <? extends SomeClass>
. Une telle référence marquée peut pointer sur un type signé avec <AnySubclassOfSomeClass>
. Mais générique polymorphe fait cette référence en lecture seule. Le compilateur vous permet d'utiliser des types génériques uniquement en tant que type de méthode renvoyé (comme de simples getters), mais bloque l'utilisation de méthodes où le type générique est un argument (comme des paramètres 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 mappe sera inutile. La seule solution consiste à rendre cette méthode non générique: get(Object)
.
Compatibilité.
Avant que les génériques soient disponibles, il y avait juste get (Object o).
S'ils avaient changé cette méthode pour obtenir (<K> o), il aurait potentiellement forcé la maintenance de code massive sur Java) pour que le code de travail soit à nouveau compilé.
Ils pourraient ont introduit une méthode supplémentaire, disons get_checked (<K> o) et déconseillent l’ancienne méthode get (), de sorte qu’il existe un chemin de transition plus doux. Mais pour une raison quelconque, cela n'a pas été fait. (Nous nous trouvons actuellement dans la situation suivante: 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 ont raison, mais je pense toujours qu'ils sont faux. Aucun concepteur sensé ne pourra jamais rendre o1.equals (o2) vrai si o1 et o2 n'ont pas de superclasse commune.)
Je regardais ceci et pensais pourquoi ils l'avaient fait de cette façon. Je ne pense pas qu'aucune des réponses existantes explique pourquoi ils ne pourraient pas simplement faire en sorte que la nouvelle interface générique accepte uniquement le type approprié pour la clé. La raison en est que même s'ils ont introduit des génériques, ils n'ont PAS créé une nouvelle interface. L'interface de carte est la même ancienne carte non générique, 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 Map non générique, vous pouvez lui passer un Map<String, Customer>
et cela fonctionnerait toujours. Dans le même temps, le contrat pour get accepte Object et la nouvelle interface doit également prendre en charge ce contrat.
À mon avis, ils auraient dû ajouter une nouvelle interface et implémenter les deux sur la collection existante, mais ils ont opté pour des interfaces compatibles, même si la conception de la méthode get était pire. Notez que les collections elles-mêmes seraient compatibles avec les méthodes existantes, à la différence des interfaces.
Compatibilité ascendante, je suppose. Map
(ou HashMap
) doit encore supporter get(Object)
.
Nous procédons actuellement à une refactorisation importante et il nous manquait ce get () fortement typé pour vérifier que nous n'avions pas manqué certains get () avec l'ancien type.
Mais j’ai trouvé une solution de contournement/moche pour la vérification du temps de compilation: créer une interface Map fortement typée get, containsKey, remove ... et la placer dans le package Java.util de votre projet.
Vous obtiendrez des erreurs de compilation rien que pour appeler get (), ... avec des types incorrects, tout semble aller pour le compilateur (du moins dans Eclipse kepler).
N'oubliez pas de supprimer cette interface après vérification de votre construction, car ce n'est pas ce que vous voulez au moment de l'exécution.