По каким причинам Map.get (Object key) не является (полностью) универсальным

405

Каковы причины, по которым принято решение не иметь полностью общий метод get в интерфейсе java.util.Map<K, V>.

Чтобы прояснить вопрос, подпись метода

V get(Object key)

вместо

V get(K key)

и мне интересно, почему (то же самое для remove, containsKey, containsValue).

WMR
источник
3
Аналогичный вопрос, касающийся коллекции: stackoverflow.com/questions/104799/…
AlikElzin-kilaka
1
Удивительно. Я использую Java уже более 20 лет, и сегодня я осознаю эту проблему.
GhostCat

Ответы:

260

Как уже упоминалось, причина get()и т. Д. Не является общей, поскольку ключ получаемой записи не обязательно должен совпадать с типом объекта, которому вы передаете get(); спецификация метода требует только, чтобы они были равны. Это следует из того, как equals()метод принимает Object в качестве параметра, а не просто того же типа, что и объект.

Хотя обычно может быть верно, что многие классы equals()определили так, что его объекты могут быть равны только объектам своего собственного класса, в Java есть много мест, где это не так. Например, спецификация for List.equals()говорит, что два объекта List равны, если они оба являются списками и имеют одинаковое содержимое, даже если они являются разными реализациями List. Итак, возвращаясь к примеру в этом вопросе, в соответствии со спецификацией метода можно иметь a Map<ArrayList, Something>и для меня вызов get()с LinkedListаргументом as, и он должен получить ключ, представляющий собой список с тем же содержимым. Это было бы невозможно, если бы они get()были общими и ограничивали тип аргумента.

newacct
источник
28
Тогда почему V Get(K k)в C #?
134
Вопрос в том, если вы хотите позвонить m.get(linkedList), почему вы не определили mтип как Map<List,Something>? Я не могу придумать случай использования, в котором вызов m.get(HappensToBeEqual)без изменения Mapтипа для получения интерфейса имеет смысл.
Элазар Лейбович
58
Вау, серьезный недостаток дизайна. Вы также не получите предупреждение компилятора, облажались. Я согласен с Элазаром. Если это действительно полезно, что, как я сомневаюсь, случается часто, getByEquals (ключ объекта) звучит более разумно ...
ммм
37
Это решение, похоже, было принято на основе теоретической чистоты, а не практичности. В большинстве случаев разработчики предпочитают, чтобы аргумент ограничивался типом шаблона, а не ограничивался поддержкой граничных случаев, подобных тому, который упомянут newacct в своем ответе. Оставление неподписанных подписей создает больше проблем, чем решает.
Сэм Голдберг
14
@newacct: «совершенно безопасный тип» - это серьезное требование для конструкции, которая может непредсказуемо потерпеть неудачу во время выполнения. Не ограничивайте свой взгляд хеш-картами, которые работают с этим. TreeMapможет дать сбой, когда вы передаете объекты неправильного типа getметоду, но иногда можете проходить, например, когда карта оказывается пустой. И что еще хуже, в случае прилагаемого Comparatorв compareметоде (который имеет общую подпись!) Может быть вызвано с аргументами неправильного типа без какого - либо непроверенного предупреждения. Это является неработающим поведением.
Хольгер
105

Кевин Бурриллион (Kevin Bourrillion), замечательный Java-кодер, написал об этой проблеме в своем блоге некоторое время назад (по общему признанию, Setвместо Map). Наиболее актуальное предложение:

Как правило, методы платформы Java Collections Framework (и библиотеки Google Collections тоже) никогда не ограничивают типы своих параметров, кроме случаев, когда это необходимо для предотвращения повреждения коллекции.

Я не совсем уверен, что согласен с этим в качестве принципа - например, .NET, кажется, хорошо, требуя правильный тип ключа - но стоит следовать рассуждениям в блоге. (Упомянув .NET, стоит объяснить, что одна из причин, по которой это не является проблемой в .NET, заключается в том, что в .NET существует большая проблема более ограниченной дисперсии ...)

Джон Скит
источник
4
Apocalisp: это не так, ситуация все та же.
Кевин Бурриллион
9
@ user102008 Нет, пост не неправильный. Несмотря на то, что Integera и a Doubleникогда не могут быть равны друг другу, все еще справедливо задать вопрос, Set<? extends Number>содержит ли a значение new Integer(5).
Кевин Бурриллион
33
Я никогда не хотел проверять членство в Set<? extends Foo>. Я очень часто менял тип ключа карты, а потом разочаровывался, что компилятор не смог найти все места, где код нуждался в обновлении. Я действительно не уверен, что это правильный компромисс.
Porculus
4
@EarthEngine: это всегда было сломано. В этом весь смысл - код сломан, но компилятор не может его поймать.
Джон Скит
1
И он все еще сломан, и только что вызвал у нас ошибку ... потрясающий ответ.
GhostCat
28

Договор выражается таким образом:

Более формально, если эта карта содержит отображение ключа k на значение v, такое что (key == null? K == null: key.equals (k) ), то этот метод возвращает v; в противном случае возвращается ноль. (Может быть не более одного такого отображения.)

(мой акцент)

и как таковой, успешный поиск ключа зависит от реализации входного ключа метода равенства. Это не обязательно зависит от класса k.

Брайан Агнью
источник
4
Это также зависит от hashCode(). Без правильной реализации hashCode () хорошо реализованная реализация equals()в этом случае довольно бесполезна.
Рудольфсон
5
Полагаю, в принципе, это позволило бы вам использовать облегченный прокси для ключа, если воссоздание всего ключа было бы нецелесообразно - при условии правильной реализации equals () и hashCode ().
Билл Мичелл
5
@rudolfson: Насколько я знаю, только HashMap зависит от хеш-кода, чтобы найти правильный сегмент. Например, TreeMap использует двоичное дерево поиска и не заботится о hashCode ().
Роб
4
Строго говоря, get()не нужно принимать аргумент типа Objectдля удовлетворения контакта. Представьте, что метод get ограничен типом ключа K- договор все равно будет действительным. Конечно, использование, где тип времени компиляции не был подклассом K, теперь не сможет компилироваться, но это не делает контракт недействительным, поскольку контракты неявно обсуждают, что произойдет, если код компилируется.
BeeOnRope
16

Это применение Закона Постеля: «Будь консервативен в том, что ты делаешь, будь либеральным в том, что ты принимаешь от других».

Проверка на равенство может быть выполнена независимо от типа; equalsметод определен на Objectклассе и принимает любые в Objectкачестве параметра. Таким образом, имеет смысл для эквивалентности ключа и операций, основанных на эквивалентности ключа, принимать любой Objectтип.

Когда карта возвращает значения ключа, она сохраняет как можно больше информации о типе, используя параметр типа.

Эриксон
источник
4
Тогда почему V Get(K k)в C #?
1
Это V Get(K k)в C #, потому что это также имеет смысл. Разница между подходами Java и .NET заключается только в том, кто блокирует несоответствующие элементы. В C # это компилятор, в Java это коллекция. Я бушую по поводу несовместимых классов коллекций .NET время от времени, но Get()и Remove()только принятие соответствующего типа, безусловно, предотвращает случайную передачу неправильного значения.
Wormbo
26
Это неправильное применение Закона Постеля. Будьте либеральны в том, что вы принимаете от других, но не слишком либерально. Этот идиотский API означает, что вы не можете определить разницу между «не в коллекции» и «вы допустили статическую ошибку при печати». Многие тысячи потерянных часов программиста можно было бы предотвратить с помощью get: K -> boolean.
Судья Ментал
1
Конечно, это должно было быть contains : K -> boolean.
Судья Ментал
4
Постель был неправ .
Альнитак
13

Я думаю, что этот раздел Общего руководства объясняет ситуацию (мой акцент):

«Необходимо убедиться, что универсальный API не является чрезмерно ограничительным; он должен продолжать поддерживать первоначальный контракт API. Рассмотрим еще несколько примеров из java.util.Collection. Предварительный универсальный API выглядит следующим образом:

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

Наивная попытка сделать это:

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

Хотя это, безусловно, безопасность типа, это не соответствует первоначальному контракту API. Метод containsAll () работает с любым видом входящей коллекции. Это будет успешным, только если входящая коллекция действительно содержит только экземпляры E, но:

  • Статический тип входящей коллекции может отличаться, возможно, потому, что вызывающая сторона не знает точный тип передаваемой коллекции, или, возможно, потому что это Collection <S>, где S является подтипом E.
  • Абсолютно законно вызывать containsAll () с коллекцией другого типа. Процедура должна работать, возвращая ложь ".
Ярдена
источник
2
почему нет containsAll( Collection< ? extends E > c )тогда?
Судья Ментал
1
@JudgeMental, хотя и не приводится в качестве примера выше , также необходимо , чтобы containsAllс а , Collection<S>где Sявляется супертипом из E. Это не будет разрешено, если бы это было containsAll( Collection< ? extends E > c ). Кроме того, как это явно указано в примере, это законно передать коллекцию другого типа (с , то возвращаемое значение является false).
Давмак
Не должно быть необходимости разрешать containsAll с коллекцией супертипа E. Я утверждаю, что необходимо запретить этот вызов статической проверкой типа, чтобы предотвратить ошибку. Это глупый контракт, который, я думаю, и является точкой оригинального вопроса.
Судья Ментал
6

Причина в том, что сдерживание определяется equalsи hashCodeкакие методы Objectи оба принимают Objectпараметр. Это был ранний недостаток дизайна в стандартных библиотеках Java. В сочетании с ограничениями в системе типов Java он заставляет все, что полагается на equals и hashCode, принимать Object.

Единственный способ иметь тип безопасных хэш - таблицы и равенство в Java является сторониться Object.equalsи Object.hashCodeи использовать общий заменитель. Функциональная Java поставляется с классами типов как раз для этой цели: Hash<A>и Equal<A>. Предусмотрена оболочка для, HashMap<K, V>которая принимает Hash<K>и Equal<K>в своем конструкторе. Поэтому этот класс getи containsметоды принимают универсальный аргумент типа K.

Пример:

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
источник
4
Само по себе это не препятствует объявлению типа «get» как «V get (K key)», поскольку «Object» всегда является предком K, поэтому «key.hashCode ()» все равно будет действительным.
Финнв
1
Хотя это не мешает, я думаю, что это объясняет. Если они переключили метод equals для принудительного равенства классов, они, конечно, не могли бы сказать людям, что лежащий в основе механизм определения местоположения объекта на карте использует equals () и hashmap (), когда прототипы методов для этих методов несовместимы.
CGP
5

Совместимость.

До того, как дженерики были доступны, был только get (Object o).

Если бы они изменили этот метод, чтобы получить (<K> o), это потенциально привело бы к массовому обслуживанию кода для пользователей Java только для того, чтобы снова скомпилировать рабочий код.

Они могли бы ввести дополнительный метод, скажем, get_checked (<K> o) и отказаться от старого метода get (), чтобы был более мягкий путь перехода. Но по какой-то причине это не было сделано. (Ситуация, в которой мы сейчас находимся, заключается в том, что вам нужно установить такие инструменты, как findBugs, чтобы проверить совместимость типов между аргументом get () и объявленным типом ключа <K> карты.)

Я думаю, что аргументы, относящиеся к семантике .equals (), являются поддельными. (Технически они правильные, но я все еще думаю, что они фальшивые. Ни один здравомыслящий дизайнер никогда не сделает так, чтобы o1.equals (o2) был истинным, если у o1 и o2 нет общего суперкласса.)

Эрвин Смут
источник
4

Есть еще одна веская причина, это не может быть сделано технически, потому что это нарушает Map.

У Java есть полиморфная общая конструкция, подобная <? extends SomeClass>. Отмеченная такая ссылка может указывать на тип со знаком <AnySubclassOfSomeClass>. Но полиморфный универсальный делает эту ссылку только для чтения . Компилятор позволяет вам использовать универсальные типы только в качестве возвращаемого типа метода (например, простые методы получения), но блокирует использование методов, где универсальный тип является аргументом (например, обычные методы установки). Это означает, что если вы напишите Map<? extends KeyType, ValueType>, компилятор не позволит вам вызывать метод get(<? extends KeyType>), и карта будет бесполезна. Единственное решение , чтобы сделать этот метод не является универсальным: get(Object).

Owheee
источник
почему метод set строго типизирован?
Sentenza
если вы имеете в виду 'put': метод put () меняет карту, и он не будет доступен с обобщениями типа <? расширяет SomeClass>. Если вы это называете, вы получаете исключение при компиляции. Такая карта будет «только для чтения»
Owheee
1

Обратная совместимость, наверное. Map(или HashMap) все еще нуждается в поддержке get(Object).

Антон Гоголев
источник
13
Но тот же аргумент может быть сделан для put(который ограничивает универсальные типы). Вы получаете обратную совместимость, используя необработанные типы. Дженерики "согласны".
Тило
Лично я думаю, что наиболее вероятной причиной такого решения является обратная совместимость.
geekdenz
1

Я смотрел на это и думал, почему они так сделали. Я не думаю, что какой-либо из существующих ответов объясняет, почему они не могли просто заставить новый универсальный интерфейс принимать только правильный тип для ключа. Фактическая причина в том, что, хотя они ввели дженерики, они НЕ создали новый интерфейс. Интерфейс Map - это та же старая неуниверсальная карта, которая служит как универсальной, так и неуниверсальной версией. Таким образом, если у вас есть метод, который принимает неуниверсальную карту, вы можете передать его, Map<String, Customer>и он все равно будет работать. В то же время контракт для get принимает Object, поэтому новый интерфейс также должен поддерживать этот контракт.

По моему мнению, они должны были добавить новый интерфейс и реализовать оба на существующей коллекции, но они решили в пользу совместимых интерфейсов, даже если это означает худший дизайн для метода get. Обратите внимание, что сами коллекции будут совместимы с существующими методами, только интерфейсы - нет.

Stilgar
источник
0

Мы сейчас делаем большой рефакторинг, и нам не хватало этого строго типизированного get (), чтобы убедиться, что мы не пропустили некоторые get () со старым типом.

Но я нашел обходной / уродливый трюк для проверки времени компиляции: создайте интерфейс Map со строго типизированным get, containsKey, remove ... и поместите его в пакет java.util вашего проекта.

Вы получите ошибки компиляции только для вызова get (), ... с неправильными типами, все остальное кажется нормальным для компилятора (по крайней мере, внутри eclipse kepler).

Не забудьте удалить этот интерфейс после проверки вашей сборки, так как это не то, что вы хотите во время выполнения.

henva
источник