Pourquoi Java Map n'étend-il pas la collection?

146

J'ai été surpris par le fait que ce Map<?,?>n'est pas un fichier Collection<?>.

J'ai pensé que cela aurait beaucoup de sens s'il était déclaré comme tel:

public interface Map<K,V> extends Collection<Map.Entry<K,V>>

Après tout, Map<K,V>c'est une collection de Map.Entry<K,V>, n'est-ce pas?

Alors, y a-t-il une bonne raison pour laquelle ce n'est pas mis en œuvre en tant que tel?


Merci à Cletus pour une réponse des plus fiables, mais je me demande toujours pourquoi, si vous pouvez déjà afficher un Map<K,V>as Set<Map.Entries<K,V>>(via entrySet()), cela ne fait pas qu'étendre cette interface à la place.

Si a Mapest a Collection, quels sont les éléments? La seule réponse raisonnable est "Paires clé-valeur"

Exactement, ce interface Map<K,V> extends Set<Map.Entry<K,V>>serait génial!

mais cela fournit une Mapabstraction très limitée (et pas particulièrement utile) .

Mais si tel est le cas, pourquoi est entrySetspécifié par l'interface? Cela doit être utile en quelque sorte (et je pense qu'il est facile de plaider pour cette position!).

Vous ne pouvez pas demander à quelle valeur une clé donnée correspond, ni supprimer l'entrée pour une clé donnée sans savoir à quelle valeur elle correspond.

Je ne dis pas que c'est tout ce qu'il y a à faire Map! Il peut et doit conserver toutes les autres méthodes (sauf entrySet, qui est redondante maintenant)!

lubrifiants polygènes
la source
1
événement Set <Map.Entry <K, V >> en fait
Maurice Perry
3
Ce n'est tout simplement pas le cas. Il est basé sur une opinion (comme décrit dans la FAQ sur la conception), plutôt que sur une conclusion logique. Pour une approche alternative, regardez la conception des conteneurs dans le C ++ STL ( sgi.com/tech/stl/table_of_contents.html ) qui est basé sur l'analyse approfondie et brillante de Stepanov.
beldaz

Réponses:

123

À partir de la FAQ sur la conception d'API de collections Java :

Pourquoi Map ne prolonge-t-il pas la collection?

C'était par conception. Nous pensons que les mappages ne sont pas des collections et que les collections ne sont pas des mappages. Ainsi, cela n'a guère de sens pour Map d'étendre l'interface Collection (ou vice versa).

Si une carte est une collection, quels sont les éléments? La seule réponse raisonnable est "Paires clé-valeur", mais cela fournit une abstraction de carte très limitée (et pas particulièrement utile). Vous ne pouvez pas demander à quelle valeur une clé donnée correspond, ni supprimer l'entrée pour une clé donnée sans savoir à quelle valeur elle correspond.

La collecte pourrait être faite pour étendre Map, mais cela pose la question: quelles sont les clés? Il n'y a pas de réponse vraiment satisfaisante, et en forcer une conduit à une interface non naturelle.

Les cartes peuvent être vues comme des collections (de clés, de valeurs ou de paires), et ce fait se reflète dans les trois «opérations de vue de collection» sur Maps (keySet, entrySet et values). Bien qu'il soit, en principe, possible d'afficher une liste comme une carte mappant des index aux éléments, cela a la propriété désagréable que la suppression d'un élément de la liste change la clé associée à chaque élément avant l'élément supprimé. C'est pourquoi nous n'avons pas d'opération de vue cartographique sur les listes.

Mise à jour: Je pense que la citation répond à la plupart des questions. Cela vaut la peine de souligner la partie sur une collection d'entrées n'étant pas une abstraction particulièrement utile. Par exemple:

Set<Map.Entry<String,String>>

permettrait:

set.add(entry("hello", "world"));
set.add(entry("hello", "world 2");

(en supposant une entry()méthode qui crée une Map.Entryinstance)

Maps nécessitent des clés uniques, ce qui enfreindrait cela. Ou si vous imposez des clés uniques à une Setdes entrées, ce n'est pas vraiment une Setau sens général. C'est un Setavec d'autres restrictions.

On peut dire que la relation equals()/ hashCode()pour Map.Entryétait purement sur la clé, mais même cela a des problèmes. Plus important encore, cela ajoute-t-il vraiment de la valeur? Vous constaterez peut-être que cette abstraction s'effondre une fois que vous commencez à regarder les cas d'angle.

Il convient de noter que le HashSetest en fait mis en œuvre comme un HashMap, et non l'inverse. Ceci est purement un détail de mise en œuvre mais est néanmoins intéressant.

La principale raison entrySet()d'exister est de simplifier la traversée afin de ne pas avoir à parcourir les clés, puis à rechercher la clé. Ne le prenez pas comme une preuve prima facie que a Mapdevrait être une Setdes entrées (à mon humble avis).

cletus
la source
1
La mise à jour est très convaincante, mais en effet, la vue d'ensemble renvoyée par entrySet()prend en charge remove, alors qu'elle ne prend pas en charge add(jette probablement UnsupportedException). Donc je vois votre point, mais là encore, je vois aussi le point du PO. (Ma propre opinion est comme indiqué dans ma propre réponse ..)
Enno Shioji
1
Zwei soulève un bon point. Toutes ces complications dues à addpeuvent être gérées comme le fait la entrySet()vue: autorisez certaines opérations et n'en autorisez pas d'autres. Une réponse naturelle, bien sûr, est "Quel genre de Setne prend pas en charge add?" - eh bien, si un tel comportement est acceptable pour entrySet(), alors pourquoi n'est-il pas acceptable pour this? Cela dit, je suis déjà principalement convaincu de la raison pour laquelle ce n'est pas une si bonne idée que je le pensais autrefois, mais je pense toujours que cela mérite un débat plus approfondi, ne serait-ce que pour enrichir ma propre compréhension de ce qui fait une bonne conception d'API / POO.
polygenelubricants
3
Le premier paragraphe de la citation de la FAQ est drôle: "Nous pensons ... donc ... n'a pas de sens". Je ne pense pas que "sentir" et "sentir" forment une conclusion; o)
Peter Wippermann
La collecte pourrait être faite pour étendre la carte ? Pas sûr, pourquoi l'auteur pense Collection- Mapt-il s'étendre ?
surexchange le
11

Bien que vous ayez obtenu un certain nombre de réponses qui couvrent assez directement votre question, je pense qu'il pourrait être utile de prendre un peu de recul et d'examiner la question d'un peu plus généralement. Autrement dit, ne pas regarder spécifiquement comment la bibliothèque Java est écrite, et pourquoi elle est écrite de cette façon.

Le problème ici est que l'héritage ne modélise qu'un seul type de points communs. Si vous choisissez deux choses qui ressemblent toutes les deux à une "collection", vous pouvez probablement choisir 8 ou 10 choses qu'elles ont en commun. Si vous choisissez une autre paire d'objets «de type collection», ils auront également 8 ou 10 objets en commun - mais ce ne sera pas les mêmes 8 ou 10 objets que la première paire.

Si vous regardez une douzaine d'éléments "de type collection" différents, pratiquement chacun d'entre eux aura probablement 8 ou 10 caractéristiques en commun avec au moins un autre - mais si vous regardez ce qui est partagé entre chacun d' entre eux d'entre eux, il ne vous reste pratiquement rien.

Il s'agit d'une situation que l'héritage (en particulier l'héritage unique) ne modélise pas correctement. Il n'y a pas de ligne de démarcation nette entre celles qui sont vraiment des collections et celles qui ne le sont pas - mais si vous voulez définir une classe Collection significative, vous êtes obligé de laisser certaines d'entre elles de côté. Si vous n'en laissez que quelques-uns, votre classe Collection ne pourra fournir qu'une interface assez clairsemée. Si vous en laissez plus, vous pourrez lui donner une interface plus riche.

Certains prennent aussi l'option de dire en gros: "ce type de collection prend en charge l'opération X, mais vous n'êtes pas autorisé à l'utiliser, en dérivant d'une classe de base qui définit X, mais en essayant d'utiliser la classe dérivée 'X échoue (par exemple , en lançant une exception).

Cela laisse toujours un problème: presque indépendamment de ce que vous laissez de côté et de ce que vous avez mis, vous allez devoir tracer une ligne de démarcation entre ce que les classes sont et ce qui ne l'est pas. Peu importe où vous tracez cette ligne, il vous restera une division claire, plutôt artificielle, entre certaines choses qui sont assez similaires.

Jerry Coffin
la source
10

Je suppose que le pourquoi est subjectif.

En C #, je pense Dictionaryétend ou au moins implémente une collection:

public class Dictionary<TKey, TValue> : IDictionary<TKey, TValue>, 
    ICollection<KeyValuePair<TKey, TValue>>, IEnumerable<KeyValuePair<TKey, TValue>>, 
    IDictionary, ICollection, IEnumerable, ISerializable, IDeserializationCallback

Dans Pharo Smalltak également:

Collection subclass: #Set
Set subclass: #Dictionary

Mais il existe une asymétrie avec certaines méthodes. Par exemple, collect:will prend l'association (l'équivalent d'une entrée), tandis que do:prend les valeurs. Ils fournissent une autre méthode keysAndValuesDo:pour itérer le dictionnaire par entrée. Add:prend une association, mais remove:a été "supprimée":

remove: anObject
self shouldNotImplement 

C'est donc définitivement faisable, mais conduit à d'autres problèmes concernant la hiérarchie des classes.

Ce qui est mieux est subjectif.

Ewernli
la source
3

La réponse de cletus est bonne, mais je veux ajouter une approche sémantique. Combiner les deux n'a aucun sens, pensez au cas où vous ajoutez une paire clé-valeur via l'interface de collecte et que la clé existe déjà. L'interface Map n'autorise qu'une seule valeur associée à la clé. Mais si vous supprimez automatiquement l'entrée existante avec la même clé, la collection a après l'ajout de la même taille qu'avant - très inattendu pour une collection.

Mnementh
la source
4
Ceci est la façon dont Setfonctionne et Set ne mettre en œuvre Collection.
Lii
2

Les collections Java sont cassées. Il manque une interface, celle de Relation. Par conséquent, la carte étend la relation étend l'ensemble. Les relations (également appelées multi-maps) ont des paires nom-valeur uniques. Les cartes (également appelées «fonctions») ont des noms (ou clés) uniques qui, bien sûr, correspondent à des valeurs. Les séquences étendent les cartes (où chaque clé est un entier> 0). Les sacs (ou multi-ensembles) étendent les cartes (où chaque clé est un élément et chaque valeur est le nombre de fois que l'élément apparaît dans le sac).

Cette structure permettrait l'intersection, l'union, etc. d'une gamme de "collections". Par conséquent, la hiérarchie devrait être:

                                Set

                                 |

                              Relation

                                 |

                                Map

                                / \

                             Bag Sequence

Sun / Oracle / Java ppl - veuillez le faire la prochaine fois. Merci.

Xagyg
la source
4
J'aimerais voir les API de ces interfaces imaginaires détaillées. Je ne suis pas sûr de vouloir une séquence (aka List) où la clé était un entier; cela ressemble à un énorme succès de performance.
Lawrence Dol
C'est vrai, une séquence est une liste - par conséquent, la séquence étend la liste. Je ne sais pas si vous pouvez y accéder, mais cela peut être intéressant portal.acm.org/citation.cfm?id=1838687.1838705
xagyg
@SoftwareMonkey voici un autre lien. Le dernier lien est le lien vers le javadoc de l'API. zedlib.sourceforge.net
xagyg
@SoftwareMonkey J'admets sans vergogne que le design est ma principale préoccupation ici. Je suppose que les gourous d'Oracle / Sun pourraient optimiser le diable.
xagyg
J'ai tendance à ne pas être d'accord. Comment peut-il tenir que "Map is-a Set" si vous ne pouvez pas toujours ajouter des éléments non membres du domaine à votre ensemble (comme dans l'exemple de la réponse de @cletus).
einpoklum
1

Si vous regardez la structure de données respective, vous pouvez facilement deviner pourquoi Mapne fait pas partie de Collection. Chacun Collectionstocke une valeur unique où en tant que Mapstocke une paire clé-valeur. Les méthodes dans l' Collectioninterface sont donc incompatibles pour l' Mapinterface. Par exemple dans Collectionnous avons add(Object o). Quelle serait une telle mise en œuvre dans Map. Cela n'a pas de sens d'avoir une telle méthode Map. Au lieu de cela, nous avons une put(key,value)méthode dans Map.

Même argument va pour addAll(), remove()et les removeAll()méthodes. La raison principale est donc la différence dans la manière dont les données sont stockées dans Mapet Collection. Aussi, si vous rappelez l' Collectioninterface implémentée par l' Iterableinterface, c'est-à-dire que toute interface avec une .iterator()méthode doit retourner un itérateur qui doit nous permettre d'itérer sur les valeurs stockées dans le Collection. Maintenant, que reviendrait une telle méthode pour un Map? Itérateur clé ou itérateur de valeur? Cela n'a pas non plus de sens.

Il existe des façons dont nous pouvons parcourir les clés et les valeurs stockées dans un Mapet c'est ainsi qu'il fait partie du Collectionframework.

Mayur Ingle
la source
There are ways in which we can iterate over keys and values stores in a Map and that is how it is a part of Collection framework.Pouvez-vous montrer un exemple de code pour cela?
RamenChef
Cela a été très bien expliqué. Je comprends maintenant. Merci!
Emir Memic
Une implémentation de add(Object o)serait d'ajouter un Entry<ClassA, ClassB>objet. A Mappeut être considéré comme unCollection of Tuples
LIvanov
0

Exactement, ce interface Map<K,V> extends Set<Map.Entry<K,V>>serait génial!

En fait, si c'était le cas implements Map<K,V>, Set<Map.Entry<K,V>>, alors j'ai tendance à être d'accord ... Cela semble même naturel. Mais cela ne fonctionne pas très bien, non? Disons que nous avons HashMap implements Map<K,V>, Set<Map.Entry<K,V>, LinkedHashMap implements Map<K,V>, Set<Map.Entry<K,V>etc ... qui est tout bon, mais si vous aviez entrySet(), personne n'oubliez pas de mettre en œuvre cette méthode, et vous pouvez être sûr que vous pouvez obtenir entrySet pour une carte, alors que vous n'êtes pas si vous espérez que l'implémenteur a implémenté les deux interfaces ...

La raison pour laquelle je ne veux pas en avoir interface Map<K,V> extends Set<Map.Entry<K,V>>est simplement parce qu'il y aura plus de méthodes. Et après tout, ce sont des choses différentes, non? Aussi très pratiquement, si je frappe map.dans IDE, je ne veux pas voir .remove(Object obj), et .remove(Map.Entry<K,V> entry)parce que je ne peux pas faire hit ctrl+space, r, returnet en finir avec.

Enno Shioji
la source
Il me semble que si l'on pouvait prétendre, sémantiquement, que "Une carte est un ensemble sur Map.Entry", alors on se donnerait la peine d'implémenter la méthode pertinente; et si on ne peut pas faire cette déclaration, alors on ne le ferait pas - c'est-à-dire qu'il devrait s'agir de la peine.
einpoklum
0

Map<K,V>ne devrait pas s'étendre Set<Map.Entry<K,V>>puisque:

  • Vous ne pouvez pas ajouter différents Map.Entrys avec la même clé à la même Map, mais
  • Vous pouvez ajouter différents Map.Entrys avec la même clé à la même Set<Map.Entry>.
einpoklum
la source
... ceci est une déclaration succincte du fantôme de la deuxième partie de la réponse de @cletus.
einpoklum
-2

Droit et simple. Collection est une interface qui n'attend qu'un seul objet, alors que Map en nécessite deux.

Collection(Object o);
Map<Object,Object>
Trinadh Koya
la source
Nice Trinad, vos réponses toujours simples et courtes.
Sairam