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

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

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

V get(Object key)

вместо

V get(K key)

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

Ответ 1

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

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

Ответ 2

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

Равномерно, методы Java Рамки коллекций (и Google Библиотека коллекций тоже) никогда ограничить типы их параметров за исключением случаев, когда необходимо предотвратить сбор от сбоев.

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

Ответ 3

Контракт выражается следующим образом:

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

(мой акцент)

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

Ответ 4

Это приложение Postel Law, "быть консервативным в том, что вы делаете, быть либеральным в том, что вы принимаете от других".

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

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

Ответ 5

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

"Вы должны убедиться, что общий 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() с коллекция другого типа. рутина должна работать, возвращая false. "

Ответ 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

Ответ 7

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

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

Ответ 8

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

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

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

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

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

Ответ 9

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

Ответ 10

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

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

Ответ 11

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

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

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

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