Які причини того, що Map.get (ключ об’єкта) не є (повністю) загальним


405

Які причини обумовлені рішенням не мати повністю загального методу отримання в інтерфейсі java.util.Map<K, V>.

Для уточнення питання підпис методу є

V get(Object key)

замість

V get(K key)

і мені цікаво чому (те саме для remove, containsKey, containsValue).


3
Схожий питання про колекцію: stackoverflow.com/questions/104799 / ...
AlikElzin-kilaka


1
Дивовижний. Я використовую Java з 20+ років, і сьогодні я усвідомлюю цю проблему.
GhostCat

Відповіді:


260

Як зазначають інші, причина, чому get()тощо, не є загальною, оскільки ключ запису, який ви шукаєте, не повинен бути того ж типу, що і об'єкт, до якого ви переходите get(); специфікація методу вимагає лише рівності. Це випливає з того, як equals()метод приймає в об'єкті параметр, а не просто той самий тип, що і об'єкт.

Хоча може бути правдою, що багато класів equals()визначили так, що його об'єкти можуть бути рівними лише об'єктам власного класу, на Яві є багато місць, де це не так. Наприклад, специфікація List.equals()говорить про те, що два об'єкти List рівні, якщо вони обидва Списки і мають однаковий вміст, навіть якщо вони різні реалізації List. Так що, повертаючись до прикладу в цьому питанні, в відповідності зі специфікацією методи можна мати Map<ArrayList, Something>і для мене , щоб зателефонувати get()з в LinkedListякості аргументу, і він повинен отримати ключ , який представляє собою список з тим же вмістом. Це було б неможливо, якби get()були загальні та обмежили його тип аргументу.


28
Тоді чому V Get(K k)в C #?

134
Питання в тому, якщо ви хочете зателефонувати m.get(linkedList), чому ви не визначили mтип типу як Map<List,Something>? Я не можу придумати випадок, коли дзвонити m.get(HappensToBeEqual)без зміни Mapтипу, щоб отримати інтерфейс має сенс.
Елазар Лейбович

58
Нічого собі, серйозна вада дизайну. Ви також не отримуєте попередження про компілятор, накручене. Я згоден з Елазаром. Якщо це дійсно корисно, що, мабуть, трапляється часто, getByEquals (клавіша Object) звучить більш розумно ...
mmm

37
Це рішення, схоже, прийняте на основі теоретичної чистоти, а не практичності. У більшості звичаїв розробники швидше бачать аргумент, обмежений типом шаблону, ніж необмежену підтримку крайових випадків, таких як той, про який говорив newacct у своїй відповіді. Залишення без шаблонів підписів створює більше проблем, ніж вирішує.
Сем Голдберг

14
@newacct: "ідеально безпечний тип" - це сильна претензія на конструкцію, яка може непередбачувано вийти з ладу під час виконання. Не звужуйте свій погляд на хеш-карти, які, можливо, працюють з цим. TreeMapможе вийти з ладу, коли ви передаєте методу об'єкти неправильного типу, getале можуть здаватися періодично, наприклад, коли карта порожня. І що ще гірше, в разі додається Comparatorв compareметоді (який має загальну підпис!) Може бути викликано з аргументами неправильного типу без якого - або неперевіреного попередження. Це є непрацюючим поведінкою.
Холгер

105

Дивовижний кодер Java в Google, Кевін Боррліон, писав про це саме питання в публікації в блозі деякий час тому (правда, в контексті Setзамість цього Map). Найбільш відповідне речення:

Однозначно, методи рамки колекцій Java (і бібліотека колекцій Google також) ніколи не обмежують типи їх параметрів, за винятком випадків, коли це потрібно для запобігання розбиття колекції.

Я не зовсім впевнений, що згоден з цим як із принципом - .NET, здається, добре вимагає, наприклад, потрібного типу ключа, - але варто дотримуватися міркувань у публікації блогу. (Згадавши .NET, варто пояснити, що частина причини, чому це не проблема в .NET, полягає в тому, що більша проблема в .NET з більш обмеженою дисперсією ...)


4
Апокаліпс: це неправда, ситуація все одно.
Кевін Бурліон

9
@ user102008 Ні, повідомлення не помиляється. Незважаючи на те, що Integerа та Doubleніколи не можуть бути рівними один одному, все-таки справедливим питанням є питання, чи Set<? extends Number>містить значення 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()в даному випадку досить марний.
rudolfson

5
Я думаю, в принципі це дозволить вам використовувати легкий проксі для ключа, якби відтворення всього ключа було недоцільним - доки правильно виконані рівні () та 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.
Суддя Психічний


13

Я думаю, що цей розділ Підручника з генерики пояснює ситуацію (мій акцент):

"Вам потрібно переконатися, що загальний API не є надмірно обмежувальним; він повинен продовжувати підтримувати оригінальний контракт API. Розгляньте ще кілька прикладів з java.util.Collection.

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

Наївна спроба узагальнити це:

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

Хоча це, безумовно, безпечно для типу, воно не відповідає оригінальному контракту API. Метод containsAll () працює з будь-яким типом вхідної колекції. Це вдасться лише в тому випадку, якщо вхідна колекція дійсно містить лише екземпляри E, але:

  • Статичний тип вхідної колекції може відрізнятися, можливо, тому, що абонент не знає точного типу переданої колекції, або, можливо, тому, що це колекція <S>, де S є підтипом E.
  • Цілком легітимним є дзвінок містить Content (All) () зі збіркою іншого типу. Рутина повинна працювати, повертаючи помилкове ".

2
чому б ні containsAll( Collection< ? extends E > c ), тоді?
Психічний суддя

1
@JudgeMental, хоча і не наводиться в якості прикладу вище , також необхідно , щоб containsAllз а , Collection<S>де Sє супертіп з E. Це не було б дозволено, якби воно було containsAll( Collection< ? extends E > c ). Крім того, як це явно вказано в прикладі, це законно передати колекцію іншого типу (з , то повертається значення є false).
davmac

Не повинно бути дозволено, щоб утримував файл ContentAll зі збіркою супертипу E. Я стверджую, що потрібно заборонити цей виклик за допомогою перевірки статичного типу, щоб запобігти появі помилок. Це дурний контракт, який, на мою думку, є пунктом оригінального питання.
Суддя Психічний

6

Причина полягає в тому, що захисна оболонка визначається equalsі hashCodeякі є методи Objectі обидва приймають Objectпараметр. Це було недоліком дизайну в стандартних бібліотеках Java. У поєднанні з обмеженнями в системі типів Java вона змушує приймати все, що покладається на рівні і 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

4
Це само по собі не перешкоджає оголошенню типу "get" як "V get (K ключ)", оскільки "Object" завжди є родоначальником K, тому "key.hashCode ()" все-таки буде дійсним.
finnw

1
Хоча це не заважає, я думаю, це пояснює це. Якщо вони переключили метод рівних на примусове рівність класів, вони, звичайно, не могли сказати людям, що в основі механізму розміщення об'єкта на карті використовується рівне () та хешмап (), коли прототипи методу для цих методів не сумісні.
cgp

5

Сумісність.

Перш ніж були доступні дженерики, було просто отримати (Object o).

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

Вони могли б запровадити додатковий метод, скажімо get_ kontrole (<K> o) та зневірити старий метод get (), щоб з'явився шлях переходу ніжнішого. Але чомусь цього не було зроблено. (Зараз у нас ситуація полягає в тому, що вам потрібно встановити такі інструменти, як findBugs, щоб перевірити сумісність типу між аргументом get () та оголошеним типом ключа <K> карти.)

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


4

Є ще одна вагома причина - це неможливо зробити технічно, тому що він порушує Карту.

У Java є поліморфна родова побудова на кшталт <? extends SomeClass>. Позначена така посилання може вказувати на тип, підписаний <AnySubclassOfSomeClass>. Але поліморфний Generic робить , що посилання тільки для читання . Компілятор дозволяє використовувати загальні типи лише як метод повернення (наприклад, прості getters), але блокує використання методів, де generic type є аргументом (як звичайні задачі). Це означає, що якщо ви пишете Map<? extends KeyType, ValueType>, компілятор не дозволяє вам викликати метод get(<? extends KeyType>), і карта буде марною. Єдине рішення , щоб зробити цей метод не є універсальним: get(Object).


чому тоді встановлений метод сильно набраний?
Сентенца

якщо ви маєте на увазі "put": метод put () змінює карту, і вона не буде доступною для таких дженериків, як <? розширює SomeClass>. Якщо ви називаєте це, ви отримаєте виключення компіляції. Така карта буде "тільки для читання"
Owheee

1

Зрозуміло, зворотна сумісність. Map(або HashMap) все ще потребує підтримки get(Object).


13
Але такий же аргумент можна зробити і для put(що обмежує родові типи). Ви отримуєте зворотну сумісність, використовуючи необроблені типи. Генерики - це "відмова".
Тіло

Особисто я думаю, що найбільш вірогідною причиною такого дизайнерського рішення є зворотна сумісність.
geekdenz

1

Я дивився на це і думав, чому вони так робили. Я не думаю, що жоден із існуючих відповідей не пояснює, чому вони не могли просто змусити новий загальний інтерфейс прийняти лише відповідний тип ключа. Фактична причина полягає в тому, що, хоча вони представили дженерики, вони НЕ створили новий інтерфейс. Інтерфейс Map - це та сама стара негенерична карта, вона просто слугує як загальною, так і загальною версією. Таким чином, якщо у вас є метод, який приймає не загальну карту, ви можете передати її, Map<String, Customer>і вона все одно працюватиме. У той же час контракт на отримання приймає Object, тому новий інтерфейс повинен підтримувати і цей контракт.

На мій погляд, вони повинні були додати новий інтерфейс і реалізовані як на існуючій колекції, але вони вирішили на користь сумісних інтерфейсів, навіть якщо це означає гірший дизайн для методу get. Зауважте, що самі колекції були б сумісні з існуючими методами, лише інтерфейси не були б.


0

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

Але я знайшов обхідний / некрасивий трюк для перевірки часу компіляції: створіть інтерфейс Map із сильно набраним get, міститьKey, видаліть ... та покладіть його на java.util пакет свого проекту.

Ви отримаєте помилки компіляції лише для виклику get (), ... з неправильними типами, все, що інше здається нормальним для компілятора (принаймні, всередині eclipse kepler).

Не забудьте видалити цей інтерфейс після перевірки вашої збірки, оскільки це не те, що потрібно під час виконання.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.