Jakie są powody, dla których Map.get (klucz obiektu) nie jest (w pełni) ogólny

405

Jakie są przyczyny decyzji o braku w pełni ogólnej metody get w interfejsie java.util.Map<K, V>.

Aby wyjaśnić pytanie, podpisem metody jest

V get(Object key)

zamiast

V get(K key)

i zastanawiam się, dlaczego (to samo remove, containsKey, containsValue).

WMR
źródło
3
Podobne pytanie dotyczące kolekcji: stackoverflow.com/questions/104799/...
AlikElzin-kilaka,
1
Niesamowity. Używam Javy od ponad 20 lat i dzisiaj zdaję sobie sprawę z tego problemu.
GhostCat,

Odpowiedzi:

260

Jak wspomnieli inni, powód, dla którego get()itp. Nie jest ogólny, ponieważ klucz wpisu, który pobierasz, nie musi być tego samego typu co obiekt, do którego przekazujesz get(); specyfikacja metody wymaga tylko, aby były one równe. Wynika to ze sposobu, w jaki equals()metoda przyjmuje obiekt jako parametr, a nie tylko tego samego typu co obiekt.

Chociaż może być prawdą, że wiele klas equals()zdefiniowało tak, że jej obiekty mogą być równe tylko obiektom własnej klasy, w Javie jest wiele miejsc, w których tak nie jest. Na przykład specyfikacja dla List.equals()mówi, że dwa obiekty List są równe, jeśli oba są Listami i mają tę samą zawartość, nawet jeśli są różnymi implementacjami List. Wracając do przykładu w tym pytaniu, zgodnie ze specyfikacją metody, możliwe jest Map<ArrayList, Something>wywołanie a dla mnie get()z LinkedListargumentem as, i powinien on pobrać klucz, który jest listą o tej samej zawartości. Nie byłoby to możliwe, gdyby get()były ogólne i ograniczyłyby typ argumentu.

newacct
źródło
28
Więc dlaczego jest V Get(K k)w C #?
134
Pytanie brzmi: jeśli chcesz zadzwonić m.get(linkedList), dlaczego nie zdefiniowałeś mtypu jako Map<List,Something>? Nie mogę wymyślić przypadku, w którym dzwonienie m.get(HappensToBeEqual)bez zmiany Maptypu w celu uzyskania interfejsu ma sens.
Elazar Leibovich
58
Wow, poważna wada konstrukcyjna. Nie dostaniesz też ostrzeżenia o kompilatorze, zepsute. Zgadzam się z Elazarem. Jeśli jest to naprawdę przydatne, co, jak sądzę, zdarza się często, getByEquals (klawisz Object) brzmi bardziej rozsądnie ...
mmm,
37
Wydaje się, że ta decyzja została podjęta na podstawie teoretycznej czystości, a nie praktyczności. W przypadku większości zastosowań programiści woleliby raczej, aby argument był ograniczony przez typ szablonu, niż nieograniczony do obsługi przypadków skrajnych, takich jak ten, o którym wspominał newacct w swojej odpowiedzi. Pozostawienie podpisów bez szablonów stwarza więcej problemów niż rozwiązuje.
Sam Goldberg,
14
@newacct: „bezpieczny typ idealnie” jest silnym argumentem za konstrukcją, która może zawieść nieprzewidywalnie w czasie wykonywania. Nie zawężaj widoku do map skrótów, które akurat z tym współpracują. TreeMapmoże się nie powieść, gdy do getmetody zostaną przekazane obiekty niewłaściwego typu , ale może się ona czasami zdarzyć, np. gdy mapa jest pusta. A co gorsza, w przypadku wystąpienia problemu dostarczany Comparatorz comparemetody (który ma sygnaturę rodzajowe!) Może być wywołana z argumentami niewłaściwego typu bez niesprawdzony ostrzeżenie. To jest zepsute zachowanie.
Holger,
105

Niesamowity programista Java w Google, Kevin Bourrillion, pisał o tym właśnie problemie w blogu jakiś czas temu (co prawda w kontekście Setzamiast Map). Najważniejsze zdanie:

Jednakowo metody Java Collections Framework (a także Biblioteka Google Maps Library) nigdy nie ograniczają rodzajów ich parametrów, z wyjątkiem sytuacji, gdy jest to konieczne, aby zapobiec uszkodzeniu kolekcji.

Nie jestem do końca pewien, czy zgadzam się z tym co do zasady - .NET wydaje się być w porządku, wymagając na przykład odpowiedniego typu klucza - ale warto podążać za rozumowaniem w poście na blogu. (Wspomniawszy o .NET, warto wyjaśnić tę część powodu, dla którego nie jest to problem w .NET, ponieważ jest większy problem w .NET o bardziej ograniczonej wariancji ...)

Jon Skeet
źródło
4
Apokalipsa: to nieprawda, sytuacja jest nadal taka sama.
Kevin Bourrillion
9
@ user102008 Nie, post nie jest błędny. Mimo że an Integeri a Doublenigdy nie mogą być sobie równe, to wciąż jest uczciwe pytanie, czy a Set<? extends Number>zawiera wartość new Integer(5).
Kevin Bourrillion
33
Nigdy nie chciałem sprawdzić członkostwa w Set<? extends Foo>. Bardzo często zmieniałem kluczowy typ mapy, a potem byłem sfrustrowany, że kompilator nie może znaleźć wszystkich miejsc, w których kod wymaga aktualizacji. Naprawdę nie jestem przekonany, że jest to właściwy kompromis.
Porculus,
4
@EarthEngine: Zawsze było zepsute. O to chodzi - kod jest zepsuty, ale kompilator nie może go złapać.
Jon Skeet
1
I nadal jest zepsuty, i właśnie spowodował nam błąd ... niesamowita odpowiedź.
GhostCat
28

Umowa jest wyrażona w następujący sposób:

Bardziej formalnie, jeśli ta mapa zawiera mapowanie z klucza k na wartość v taką, że (klucz == null? K == null: key.equals (k) ), wówczas ta metoda zwraca v; w przeciwnym razie zwraca null. (Może być maksymalnie jedno takie mapowanie).

(mój nacisk)

i jako takie, pomyślne wyszukiwanie klucza zależy od implementacji metody równości w kluczu wejściowym. Nie jest niekoniecznie zależy od klasy k.

Brian Agnew
źródło
4
Zależy również od hashCode(). Bez właściwej implementacji hashCode () ładnie zaimplementowana equals()jest w tym przypadku raczej bezużyteczna.
rudolfson
5
Myślę, że w zasadzie pozwoliłoby to na użycie lekkiego proxy dla klucza, jeśli odtworzenie całego klucza było niepraktyczne - pod warunkiem, że poprawnie zaimplementowano equals () i hashCode ().
Bill Michell
5
@rudolfson: O ile mi wiadomo, tylko HashMap jest zależny od kodu skrótu w celu znalezienia właściwego segmentu. TreeMap na przykład używa binarnego drzewa wyszukiwania i nie dba o hashCode ().
Rob
4
Ściśle mówiąc, get()nie trzeba brać argumentu typu, Objectaby zadowolić kontakt. Wyobraź sobie, że metoda get była ograniczona do typu klucza K- umowa nadal byłaby ważna. Oczywiście zastosowania, w których typ czasu kompilacji nie był podklasą, nie Kmogłyby się teraz skompilować, ale nie unieważnia to umowy, ponieważ umowy domyślnie omawiają, co się stanie, jeśli kod się skompiluje.
BeeOnRope
16

Jest to zastosowanie prawa Postela: „bądź konserwatywny w tym, co robisz, bądź liberalny w tym, co akceptujesz od innych”.

Kontrole równości można przeprowadzać niezależnie od rodzaju; equalssposób określa się na Objectklasy i przyjmuje każdy Objectjako parametr. Tak więc sensowne jest, aby akceptować dowolność klucza i operacje oparte na równoważności kluczaObject typ.

Kiedy mapa zwraca kluczowe wartości, zachowuje tyle informacji o typie, ile może, używając parametru type.

erickson
źródło
4
Więc dlaczego jest V Get(K k)w C #?
1
Jest V Get(K k)w C #, ponieważ ma to również sens. Różnica między metodami Java i .NET polega na tym, że blokuje niepasujące elementy. W C # jest to kompilator, w Javie jest to kolekcja. I wściekłość o niespójnych klas kolekcji .NET jest raz na jakiś czas, ale Get()i Remove()tylko przyjmując typ pasujący na pewno zapobiega przypadkowemu przekazując błędną wartość w.
Wormbo
26
To niewłaściwe stosowanie prawa Postela. Bądź liberalny w tym, co akceptujesz od innych, ale nie zbyt liberalny. Ten idiotyczny interfejs API oznacza, że ​​nie można odróżnić „nie ma w kolekcji” od „popełniłeś błąd w pisaniu statycznym”. Wielu tysiącom utraconych godzin pracy programisty można było zapobiec za pomocą get: K -> boolean.
Sędzia Mental
1
Oczywiście, że powinno być contains : K -> boolean.
Sędzia Mental
4
Postel się mylił .
Alnitak,
13

Myślę, że ta sekcja samouczka generycznego wyjaśnia sytuację (mój nacisk):

„Musisz upewnić się, że ogólny interfejs API nie jest nadmiernie restrykcyjny; musi nadal obsługiwać pierwotną umowę API. Ponownie rozważ kilka przykładów z java.util.Collection. Wstępnie ogólny interfejs API wygląda następująco:

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

Naiwna próba jej wygenerowania to:

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

Chociaż jest to z pewnością bezpieczne dla typu, nie jest zgodne z pierwotną umową API. Metoda zawieraAll () działa z każdym rodzajem kolekcji przychodzącej. Uda się to tylko wtedy, gdy przychodząca kolekcja naprawdę zawiera tylko instancje E, ale:

  • Statyczny typ kolekcji przychodzącej może się różnić, być może dlatego, że wywołujący nie zna dokładnego typu przekazywanej kolekcji, a może dlatego, że jest to kolekcja <S>, gdzie S jest podtypem E.
  • Wywołanie funkcji zawieraAll () z kolekcją innego typu jest całkowicie uzasadnione. Procedura powinna działać, zwracając wartość false. ”
Jardena
źródło
2
dlaczego więc nie containsAll( Collection< ? extends E > c )?
Sędzia Mental
1
@JudgeMental, chociaż nie jako przykład powyżej, jest również konieczne, aby containsAllz A Collection<S>, gdzie Sjest supertypem z E. Gdyby tak było, nie byłoby to dozwolone containsAll( Collection< ? extends E > c ). Ponadto, jak to wyraźnie podane w tym przykładzie jest to uzasadnione przechodzą zbierania różnych typów (o wartość powrotna jest wówczas false).
davmac
Nie powinno być konieczne, aby zezwolić na zawartość zawiera wszystko z kolekcją nadtypu E. Argumentuję, że konieczne jest wyłączenie tego wywołania za pomocą statycznego sprawdzania typu, aby zapobiec błędowi. To głupia umowa, która moim zdaniem jest sednem pierwotnego pytania.
Sędzia Mental
6

Powodem jest to, że ograniczenie jest określane przez equalsi hashCodektóre metody są włączone Objecti oba przyjmują Objectparametr. To była wczesna wada projektowa w standardowych bibliotekach Java. W połączeniu z ograniczeniami w systemie typów Java, wymusza wszystko, co zależy od equals i hashCodeObject .

Jedynym sposobem, aby mieć bezpieczny typu tabele mieszania i równości w Javie jest wystrzegać Object.equalsi Object.hashCodei używać rodzajowe substytut. Funkcjonalna Java jest dostarczana z klasami tylko w tym celu: Hash<A>i Equal<A>. Zapewnione HashMap<K, V>jest opakowanie dla, które pobiera Hash<K>i Equal<K>jego konstruktora. Dlatego te klasy geti containsmetody przyjmują ogólny argument typuK .

Przykład:

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
Apokalipsa
źródło
4
To samo w sobie nie uniemożliwia zadeklarowania typu „get” jako „V get (klucz K)”, ponieważ „Object” jest zawsze przodkiem K, więc „key.hashCode ()” nadal będzie poprawne.
finnw
1
Chociaż to nie przeszkadza, myślę, że to wyjaśnia. Jeśli zmienili metodę equals, aby wymusić równość klas, z pewnością nie mogliby powiedzieć ludziom, że podstawowy mechanizm lokalizacji obiektu na mapie wykorzystuje equals () i hashmap (), gdy prototypy metod dla tych metod nie są kompatybilne.
cgp
5

Zgodność.

Zanim dostępne były leki generyczne, pojawiło się tylko (Obiekt o).

Gdyby zmienili tę metodę, aby uzyskać (<K> o), potencjalnie zmusiłoby to masową konserwację kodu do użytkowników Java, aby ponownie skompilować działający kod.

Oni mogli wprowadziliśmy dodatkowy sposób, powiedzmy get_checked (<K> O) i deprecate sposób stary get (), więc był łagodniejszy ścieżki przejścia. Ale z jakiegoś powodu tego nie zrobiono. (Obecna sytuacja polega na tym, że musisz zainstalować narzędzia takie jak findBugs, aby sprawdzić zgodność typu między argumentem get () a zadeklarowanym typem klucza <K> mapy).

Argumenty dotyczące semantyki .equals () są, jak sądzę, fałszywe. (Technicznie są poprawne, ale nadal uważam, że są fałszywe. Żaden projektant przy zdrowych zmysłach nigdy nie sprawi, że o1. Równe (o2) będą prawdziwe, jeśli o1 i o2 nie będą miały żadnej wspólnej nadklasy.)

Erwin Smout
źródło
4

Jest jeszcze jeden ważny powód, którego nie można zrobić technicznie, ponieważ psuje mapę.

Java ma polimorficzną ogólną budowę podobną do <? extends SomeClass>. Oznaczone takie odniesienie może wskazywać na typ podpisany za pomocą <AnySubclassOfSomeClass>. Ale polimorficzny rodzajowy sprawia, że ​​odniesienie jest tylko do odczytu . Kompilator pozwala używać typów ogólnych tylko jako zwracanej metody (jak proste metody pobierające), ale blokuje metody, w których typem ogólnym jest argument (jak zwykłe ustawiacze). Oznacza to, że jeśli napiszesz Map<? extends KeyType, ValueType>, kompilator nie pozwala na wywołanie metody get(<? extends KeyType>), a mapa będzie bezużyteczna. Jedynym rozwiązaniem jest, aby metoda ta ogólna: get(Object).

Owheee
źródło
dlaczego zatem metoda set jest mocno wpisana?
Sentenza
jeśli masz na myśli „put”: metoda put () zmienia mapę i nie będzie dostępna z takimi rodzajami jak <? rozszerza SomeClass>. Jeśli go nazwiesz, masz wyjątek kompilacji. Taka mapa będzie „tylko do odczytu”
Owheee,
1

Chyba kompatybilność wsteczna. Map(lub HashMap) nadal musi wspierać get(Object).

Anton Gogolev
źródło
13
Można jednak wysunąć ten sam argument put(który ogranicza typy ogólne). Kompatybilność wsteczna jest uzyskiwana przy użyciu typów raw. Ogólne to „opt-in”.
Thilo,
Osobiście uważam, że najbardziej prawdopodobną przyczyną tej decyzji projektowej jest zgodność wsteczna.
geekdenz
1

Patrzyłem na to i myślałem, dlaczego to zrobili w ten sposób. Nie sądzę, żeby którakolwiek z istniejących odpowiedzi wyjaśniała, dlaczego nie mogli po prostu sprawić, aby nowy ogólny interfejs akceptował tylko odpowiedni typ klucza. Rzeczywisty powód jest taki, że chociaż wprowadzili leki generyczne, NIE utworzyli nowego interfejsu. Interfejs mapy jest tą samą starą nie-ogólną mapą, która służy tylko jako wersja ogólna i ogólna. W ten sposób, jeśli masz metodę, która akceptuje mapę ogólną, możesz ją przekazać Map<String, Customer>i nadal będzie działać. Jednocześnie umowa get get przyjmuje Object, więc nowy interfejs powinien również obsługiwać tę umowę.

Moim zdaniem powinni oni dodać nowy interfejs i zaimplementować oba w istniejącej kolekcji, ale zdecydowali się na kompatybilne interfejsy, nawet jeśli oznacza to gorszy projekt dla metody get. Zauważ, że same kolekcje byłyby kompatybilne z istniejącymi metodami, których nie byłyby dostępne tylko interfejsy.

Stilgar
źródło
0

Obecnie dokonujemy dużego refaktoryzacji i brakowało nam tego mocno wpisanego get (), aby sprawdzić, czy nie przegapiliśmy trochę get () ze starym typem.

Ale znalazłem obejście / brzydką sztuczkę sprawdzania czasu kompilacji: utwórz interfejs mapy z silnie wpisanym get, zawiera klucz, usuń ... i umieść go w pakiecie java.util twojego projektu.

Otrzymasz błędy kompilacji tylko przy wywołaniu get (), ... przy niewłaściwych typach, wszystko inne wydaje się w porządku dla kompilatora (przynajmniej wewnątrz keplera zaćmienia).

Nie zapomnij usunąć tego interfejsu po sprawdzeniu kompilacji, ponieważ nie jest to pożądane w środowisku wykonawczym.

henva
źródło