Dlaczego Java Map nie rozszerza kolekcji?

146

Zaskoczył mnie fakt, że Map<?,?>nie jest to plik Collection<?>.

Pomyślałem, że miałoby to dużo sensu, gdyby zostało to zadeklarowane jako takie:

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

W końcu a Map<K,V>to zbiór Map.Entry<K,V>, prawda?

Czy jest więc dobry powód, dla którego nie jest zaimplementowany jako taki?


Dziękuję Cletusowi za najbardziej autorytatywną odpowiedź, ale wciąż zastanawiam się, dlaczego, jeśli już możesz zobaczyć Map<K,V>as Set<Map.Entries<K,V>>(via entrySet()), zamiast tego nie tylko rozszerza ten interfejs.

Jeśli a Mapto a Collection, jakie są elementy? Jedyną rozsądną odpowiedzią jest „Pary klucz-wartość”

Dokładnie, interface Map<K,V> extends Set<Map.Entry<K,V>>byłoby świetnie!

ale zapewnia to bardzo ograniczoną (i niezbyt użyteczną) Mapabstrakcję.

Ale jeśli tak jest, to dlaczego jest entrySetokreślony przez interfejs? To musi być jakoś przydatne (i myślę, że łatwo jest polemizować o takie stanowisko!).

Nie możesz zapytać, na jaką wartość jest przypisany dany klucz, ani nie możesz usunąć wpisu dla danego klucza, nie wiedząc, na jaką wartość jest mapowany.

Nie mówię, że to wszystko Map! Może i powinien zachować wszystkie inne metody (z wyjątkiem tej entrySet, która jest teraz zbędna)!

smary wielogenowe
źródło
1
event Set <Map.Entry <K, V >> właściwie
Maurice Perry
3
Po prostu nie. Opiera się na opinii (jak opisano w FAQ projektowania), a nie na logicznym wniosku. Aby uzyskać alternatywne podejście, spójrz na projekt kontenerów w C ++ STL ( sgi.com/tech/stl/table_of_contents.html ), który jest oparty na dokładnej i błyskotliwej analizie Stepanova.
beldaz

Odpowiedzi:

123

Z często zadawanych pytań dotyczących projektowania interfejsu API kolekcji Java :

Dlaczego mapa nie rozszerza kolekcji?

To było zamierzone. Uważamy, że mapowania nie są kolekcjami, a kolekcje nie są mapowaniami. Dlatego nie ma sensu, aby Map rozszerzał interfejs Collection (lub odwrotnie).

Jeśli mapa jest zbiorem, jakie są elementy? Jedyną rozsądną odpowiedzią są „pary klucz-wartość”, ale zapewnia to bardzo ograniczoną (i niezbyt użyteczną) abstrakcję mapy. Nie możesz zapytać, na jaką wartość jest przypisany dany klucz, ani nie możesz usunąć wpisu dla danego klucza, nie wiedząc, na jaką wartość jest mapowany.

Kolekcję można by rozszerzyć o Mapę, ale pojawia się pytanie: jakie są klucze? Nie ma naprawdę satysfakcjonującej odpowiedzi, a wymuszenie jednej prowadzi do nienaturalnego interfejsu.

Mapy można wyświetlać jako kolekcje (kluczy, wartości lub par), a fakt ten znajduje odzwierciedlenie w trzech „operacjach widoku kolekcji” w Mapach (zestaw kluczy, zestaw pozycji i wartości). Chociaż w zasadzie możliwe jest przeglądanie listy jako mapy mapowania indeksów do elementów, ma to nieprzyjemną właściwość polegającą na tym, że usunięcie elementu z listy zmienia klucz skojarzony z każdym elementem przed usuniętym elementem. Dlatego nie mamy operacji widoku mapy na listach.

Aktualizacja: myślę, że cytat odpowiada na większość pytań. Warto podkreślić, że zbiór haseł nie jest abstrakcją szczególnie przydatną. Na przykład:

Set<Map.Entry<String,String>>

pozwoliłoby:

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

(zakładając entry()metodę, która tworzy Map.Entryinstancję)

Mapwymagają unikalnych kluczy, więc naruszałoby to. Lub jeśli narzucisz unikalne klucze na jeden Setz wpisów, nie jest to tak naprawdę Setw ogólnym sensie. Jest Setz dalszymi ograniczeniami.

Prawdopodobnie można powiedzieć, że związek equals()/ hashCode()dla Map.Entrybył wyłącznie kluczowy, ale nawet to ma problemy. Co ważniejsze, czy naprawdę dodaje jakąś wartość? Może się okazać, że ta abstrakcja się załamie, gdy zaczniesz patrzeć na narożne przypadki.

Warto zauważyć, że HashSetfaktycznie jest zaimplementowany jako a HashMap, a nie na odwrót. To jest tylko szczegół dotyczący implementacji, ale mimo to jest interesujący.

Głównym powodem entrySet()istnienia jest uproszczenie przechodzenia, aby nie trzeba było przechodzić przez klucze, a następnie wyszukiwać klucza. Nie traktuj tego jako dowodu prima facie, że a Mappowinno być a Setz wpisów (imho).

cletus
źródło
1
Aktualizacja jest bardzo przekonująca, ale rzeczywiście, widok Set zwrócony przez entrySet()obsługuje remove, podczas gdy nie obsługuje add(prawdopodobnie wyrzuca UnsupportedException). Więc rozumiem twój punkt widzenia, ale z drugiej strony widzę także punkt PO ... (Moja własna opinia jest taka, jak podano w mojej własnej odpowiedzi ..)
Enno Shioji Kwietnia
1
Zwei ma dobry punkt widzenia. Wszystkie te komplikacje addmogą być obsługiwane tak samo, jak entrySet()widok: zezwalaj na niektóre operacje, a nie na inne. Naturalną odpowiedzią jest oczywiście „Jaki rodzaj Setnie obsługuje add?” - cóż, jeśli takie zachowanie jest do zaakceptowania entrySet(), to dlaczego nie jest do przyjęcia this? To powiedziawszy, jestem już w większości przekonany, dlaczego nie jest to taki wspaniały pomysł, jak kiedyś myślałem, ale nadal uważam, że warto go dalszej debaty, choćby po to, aby wzbogacić moje własne zrozumienie tego, co składa się na dobry projekt API / OOP.
polygenelubricants
3
Pierwszy akapit cytatu z FAQ jest zabawny: „Czujemy, że… więc… nie ma sensu”. Nie czuję, że „czuję” i „sens” kończą się wnioskiem; o)
Peter Wippermann
Czy kolekcję można rozszerzyć o mapę ? Nie jestem pewien, dlaczego autor myśli o Collectionprzedłużeniu Map?
przewyższenie
11

Chociaż otrzymałeś wiele odpowiedzi, które dość bezpośrednio pokrywają twoje pytanie, myślę, że warto byłoby nieco cofnąć się i spojrzeć na pytanie bardziej ogólnie. To znaczy, aby nie patrzeć konkretnie na to, jak biblioteka Java została napisana, i przyjrzeć się, dlaczego jest napisana w ten sposób.

Problem polega na tym, że dziedziczenie modeluje tylko jeden typ podobieństwa. Jeśli wybierzesz dwie rzeczy, które wydają się „podobne do kolekcji”, prawdopodobnie możesz wybrać 8 lub 10 rzeczy, które mają ze sobą wspólnego. Jeśli wybierzesz inną parę „kolekcjonerskich” rzeczy, będą one również mieć 8 lub 10 wspólnych elementów - ale nie będą to takie same 8 lub 10 rzeczy, co pierwsza para.

Jeśli spojrzeć na kilkunastu różnych „kolekcja-like” rzeczy, praktycznie każdy z nich będzie prawdopodobnie mieć coś jak 8 lub 10 cech wspólnych z co najmniej jednym drugim - ale jeśli spojrzeć na to, co wspólne w poprzek każdego jednego z nich praktycznie nic nie zostało.

Jest to sytuacja, w której dziedziczenie (zwłaszcza dziedziczenie pojedyncze) po prostu nie jest dobrym modelem. Nie ma wyraźnej linii podziału między tym, które z nich są naprawdę kolekcjami, a które nie - ale jeśli chcesz zdefiniować sensowną klasę kolekcji, jesteś skazany na pozostawienie niektórych z nich. Jeśli zostawisz tylko kilka z nich, twoja klasa Collection będzie w stanie zapewnić dość rzadki interfejs. Jeśli pominiesz więcej, będziesz w stanie zapewnić bogatszy interfejs.

Niektórzy przyjmują również opcję mówiącą po prostu: „ten typ kolekcji obsługuje operację X, ale nie wolno jej używać, poprzez wyprowadzenie z klasy bazowej, która definiuje X, ale próba użycia klasy pochodnej 'X kończy się niepowodzeniem (np. , zgłaszając wyjątek).

To wciąż pozostawia jeden problem: prawie niezależnie od tego, które z nich pominiesz, a które włożysz, będziesz musiał wyznaczyć sztywną granicę między tym, jakie zajęcia są, a co nie. Bez względu na to, gdzie narysujesz tę linię, pozostanie ci jasny, raczej sztuczny podział na niektóre rzeczy, które są dość podobne.

Jerry Coffin
źródło
10

Myślę, że powód jest subiektywny.

W C # myślę, że Dictionaryrozszerza lub przynajmniej implementuje kolekcję:

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

Również w Pharo Smalltak:

Collection subclass: #Set
Set subclass: #Dictionary

Ale istnieje asymetria w przypadku niektórych metod. Na przykład collect:will przyjmuje asocjację (odpowiednik wpisu), podczas gdy do:przyjmuje wartości. Zapewniają inną metodę keysAndValuesDo:iteracji słownika według pozycji. Add:przyjmuje skojarzenie, ale remove:zostało „zablokowane”:

remove: anObject
self shouldNotImplement 

Jest to więc ostatecznie wykonalne, ale prowadzi do innych problemów dotyczących hierarchii klas.

To, co jest lepsze, jest subiektywne.

ewernli
źródło
3

Odpowiedź cletus jest dobra, ale chcę dodać podejście semantyczne. Łączenie obu nie ma sensu, pomyśl o przypadku, gdy dodajesz parę klucz-wartość za pośrednictwem interfejsu kolekcji, a klucz już istnieje. Interfejs Map dopuszcza tylko jedną wartość skojarzoną z kluczem. Ale jeśli automatycznie usuniesz istniejący wpis z tym samym kluczem, po dodaniu kolekcja będzie miała taki sam rozmiar jak poprzednio - bardzo nieoczekiwane dla kolekcji.

Mnementh
źródło
4
Oto jak to Setdziała, a Set nie realizować Collection.
Lii
2

Kolekcje Java są uszkodzone. Brakuje interfejsu Relation. Stąd Mapa rozszerza Relation rozszerza Set. Relacje (nazywane również multi-mapami) mają unikalne pary nazwa-wartość. Mapy (zwane też „funkcjami”) mają unikalne nazwy (lub klucze), które oczywiście odwzorowują wartości. Sekwencje rozszerzają mapy (gdzie każdy klucz jest liczbą całkowitą> 0). Torby (lub zestawy wielokrotne) rozszerzają mapy (gdzie każdy klucz jest elementem, a każda wartość to liczba razy, gdy element pojawia się w torbie).

Taka struktura umożliwiłaby przecięcie, połączenie itp. Szeregu „kolekcji”. Stąd hierarchia powinna wyglądać następująco:

                                Set

                                 |

                              Relation

                                 |

                                Map

                                / \

                             Bag Sequence

Sun / Oracle / Java ppl - pobierz to dobrze następnym razem. Dzięki.

xagyg
źródło
4
Chciałbym zobaczyć szczegółowo API dla tych wyimaginowanych interfejsów. Nie jestem pewien, czy chciałbym sekwencję (aka List), w której klucz był liczbą całkowitą; brzmi to jak wielki hit.
Lawrence Dol
Zgadza się, sekwencja jest listą - stąd Sequence rozszerza List. Nie wiem, czy możesz uzyskać dostęp do tego, ale może to być interesujące portal.acm.org/citation.cfm?id=1838687.1838705
xagyg
@SoftwareMonkey tutaj jest kolejny link. Ostatni link to link do javadoc interfejsu API. zedlib.sourceforge.net
xagyg
@SoftwareMonkey Bez wstydu przyznaję, że projekt jest tutaj moim głównym zmartwieniem. Zakładam, że guru w Oracle / Sun mogą to zoptymalizować.
xagyg
Raczej się nie zgadzam. Jak może utrzymywać, że „Mapa jest zbiorem”, jeśli nie możesz zawsze dodawać do zestawu elementów domeny niebędących członkami (jak w przykładzie w odpowiedzi @cletus).
einpoklum
1

Jeśli spojrzysz na odpowiednią strukturę danych, możesz łatwo zgadnąć, dlaczego Mapnie jest ona częścią Collection. Każdy Collectionprzechowuje pojedynczą wartość, gdzie jako Mapprzechowuje parę klucz-wartość. Zatem metody w Collectioninterfejsie są niekompatybilne z Mapinterfejsem. Na przykład w Collectionmamy add(Object o). Jaka byłaby taka implementacja w Map. Nie ma sensu mieć takiej metody Map. Zamiast tego mamy put(key,value)metodę w Map.

Ten sam argument odnosi się do addAll(), remove()i removeAll()metod. Więc głównym powodem jest różnica w sposobie przechowywania danych w Mapi Collection. Również jeśli pamiętasz Collectioninterfejs zaimplementowany w Iterableinterfejsie, tj. Każdy interfejs z .iterator()metodą powinien zwracać iterator, który musi pozwolić nam na iterację wartości przechowywanych w Collection. Co teraz taka metoda zwróci Map? Iterator klucza czy Iterator wartości? To też nie ma sensu.

Istnieją sposoby na iterację kluczy i wartości przechowywanych w a Mapi tak jest to część Collectionframeworka.

Mayur Ingle
źródło
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.Czy możesz pokazać przykładowy kod?
RamenChef
To było bardzo dobrze wyjaśnione. Teraz rozumiem. Dzięki!
Emir Memic
Implementacja add(Object o)polegałaby na dodaniu Entry<ClassA, ClassB>obiektu. A Mapmożna uznać zaCollection of Tuples
LIvanov
0

Dokładnie, interface Map<K,V> extends Set<Map.Entry<K,V>>byłoby świetnie!

Właściwie, gdyby tak było implements Map<K,V>, Set<Map.Entry<K,V>>, to raczej się zgadzam. Wydaje się to nawet naturalne. Ale to nie działa zbyt dobrze, prawda? Powiedzmy, że mamy HashMap implements Map<K,V>, Set<Map.Entry<K,V>, LinkedHashMap implements Map<K,V>, Set<Map.Entry<K,V>itd ... to wszystko dobrze, ale gdybyś to zrobił entrySet(), nikt nie zapomni o wdrożeniu tej metody i możesz być pewien, że możesz uzyskać entrySet dla dowolnej mapy, podczas gdy nie jesteś, jeśli masz nadzieję że osoba wdrażająca zaimplementowała oba interfejsy ...

Powód, dla którego nie chcę, interface Map<K,V> extends Set<Map.Entry<K,V>>jest po prostu taki, że będzie więcej metod. W końcu to różne rzeczy, prawda? Również bardzo praktycznie, jeśli trafię map.w IDE, nie chcę tego widzieć .remove(Object obj), a .remove(Map.Entry<K,V> entry)ponieważ nie mogę tego zrobić hit ctrl+space, r, returni skończyć z tym.

Enno Shioji
źródło
Wydaje mi się, że gdyby ktoś mógł twierdzić semantycznie, że „Mapa jest zbiorem na Map.Entry's”, to należałoby zawracać sobie głowę implementacją odpowiedniej metody; a jeśli nie można tego wypowiedzieć, to nie - tj. powinno chodzić o kłopoty.
einpoklum
0

Map<K,V>nie powinien przedłużać się, Set<Map.Entry<K,V>>ponieważ:

  • Nie możesz dodać różnych Map.Entryznaków tym samym kluczem do tego samego Map, ale
  • Ty można dodawać różne Map.Entrys z tym samym kluczem do tego samego Set<Map.Entry>.
einpoklum
źródło
... to jest zwięzłe stwierdzenie sedna drugiej części odpowiedzi @cletus.
einpoklum
-2

Prosto i prosto. Kolekcja to interfejs, który oczekuje tylko jednego obiektu, podczas gdy mapa wymaga dwóch.

Collection(Object o);
Map<Object,Object>
Trinadh Koya
źródło
Ładny Trinad, twoje odpowiedzi są zawsze proste i krótkie.
Sairam