Что не так с этим дизайном? Переопределение или перегрузка java.util. HashMap

Этот вопрос задали мне в интервью MS. Я хочу знать точные вопросы проектирования в этой части кода. Код был уже дан, должен был найти вопросы проектирования.

У меня есть класс MyHashMap, который расширяет класс java HashMap. В классе MyHashMap я должен хранить некоторую информацию сотрудников. Ключ в этой карте будет firstName+lastName+Address.

public MyHashMap extends HashMap<Object, Object> {
  //some member variables 
  //
  public void put(String firstName, String lastName, String Address, Object obj) {
       String key =   firstName + lastName+ Address;
       put(key, obj);
  }

  public Object get(String firstName, String lastName, String Address) {
       String key =   firstName + lastName+ Address;
       return get(key);
  }

  public void remove(Strig key) {
        put(key, ""); 
  }

  //some more methods 
}

Что не так с этим дизайном? Я должен разделить HashMap на подклассы, или я должен объявить HashMap как членскую переменную этого класса? Или я должен был реализовать методы хэш-кода/равняться?

5
задан David Grant 17 February 2010 в 11:32
поделиться

7 ответов

Существует довольно много проблем, но самая большая проблема, которую я вижу, заключается в том, что вы используете конкатенированную String в качестве ключа. Следующие два вызова различны, но эквивалентны:

final MyHashMap map = new MyHashMap();

map.put("foo", "", "baz", new Object());
map.put("", "foo", "baz", new Object()); // Overwrites the previous call

Есть также проблема в том, что вы объявляете тип ключа как Object, но всегда используете String и поэтому не используете безопасность типов, которая приходит с generics. Например, если вы захотите перебрать keySet вашей Map, вам придется привести каждый Set элемент к String, но вы не сможете быть уверены, что кто-то не злоупотребит вашей Map, используя, например, Integer ключ.

Лично я бы предпочел композицию наследованию, если только у вас нет веских причин не делать этого. В вашем случае MyHashMap является перегрузкой стандартных Map методов put, get и remove, но не переопределяет ни один из них. Вы должны наследоваться от класса, чтобы изменить его поведение, но ваша реализация этого не делает, поэтому композиция - очевидный выбор.

Для примера, перегрузка, а не переопределение означает, что если вы сделаете следующее объявление:

Map<Object, Object> map = new MyHashMap();

ни один из объявленных вами методов не будет доступен. В соответствии с рекомендациями некоторых других ответов, гораздо лучше использовать объект, состоящий из firstName, lastName и address, в качестве ключа карты, но вы должны помнить о реализации equals и hashCode, иначе ваши значения нельзя будет извлечь из HashMap.

13
ответ дан 18 December 2019 в 05:43
поделиться

Я бы пошел на объявление HashMap как переменной-члена вашего класса.

Я не помню, давалось ли хорошее объяснение в «Чистом коде» или «Эффективной Java», но в основном это проще сделать, требует меньше работы, если API изменяется.

Обычно расширение такого класса означает, что вы хотите изменить его поведение.

4
ответ дан 18 December 2019 в 05:43
поделиться

Карта использует внутренний ключ на основе имени, фамилии и адреса. Таким образом, метод remove должен (1) быть реализован как remove (String firstName, String lastName, String Address) и (2) он не должен изменять поведение исходного метода удаления ( который действительно удалил запись), просто изменив значение. Лучшая реализация удаления:

public void remove(String firstName, String lastName, String address) {
   String key =   firstName + lastName + address;
   return remove(key);
}
3
ответ дан 18 December 2019 в 05:43
поделиться

Мое первое мнение:

public MyHashMap extends HashMap<Oject, Object>

это неправильно. Нет безопасности типов.

Если карта требуется, то хотя бы сделайте ее

public MyHashMap extends HashMap<NameAddress, Employee>

и настройте put и получите то же самое. В NameAddress используйте имя, фамилию и адрес в equals и hashCode.

Мне лично кажется, что лучше всего подошел бы интерфейс Set, с возможным HashMap в качестве члена. Тогда вы могли бы определить интерфейс так, как он должен быть:

public EmployeeSet implements Set<Employee> {
  private static class NameAddress {
    public boolean equals();
    public int hashCode();
  } 

  private HashMap employees = new HashMap<NameAddress, Employee>();   

  public add(Employee emp) {
    NameAddress nad = new NameAddress(emp);
    empoyees.add(nad, emp);
  }

  public remove(Employee emp) {
  }
}

и извлекать информацию об имени и адресе в рамках реализации для создания ключа.

EDIT Дэвид указал, что NameAddress не обязательно должен быть сравнимым, и я удалил эту часть интерфейса. Добавил hashCode для корректности.

3
ответ дан 18 December 2019 в 05:43
поделиться

Неправильность этой конструкции заключается прежде всего в том, что она претендует на то, чтобы быть HashMap, но на самом деле таковой не является. Перегруженные методы "заменяют" методы Map, но те по-прежнему доступны - теперь вы должны использовать класс способом, несовместимым с интерфейсом Map, и игнорировать (все еще технически возможный) совместимый способ его использования.

Лучший способ сделать это - создать класс EmployeeData с полями имени и адреса и методами hashCode() и equals() на их основе (или, еще лучше, уникальным полем ID). Тогда вам не нужен нестандартный подкласс Map - вы можете просто использовать HashMap. На самом деле, тип значения также должен быть более конкретным, чем Object.

5
ответ дан 18 December 2019 в 05:43
поделиться

Два других "гнусных преступления" против хорошей практики заключаются в том, что метод remove:

  • перегружает Map.remove(), а не переопределяет его (таким образом, получается негерметичная абстракция), и

  • реализует поведение, несовместимое с поведением метода, который он переопределяет!

Установка значения, связанного с ключом, в пустую строку НЕ ТОЖЕ САМОЕ, что удаление записи для этого ключа. (Даже установка значения в null - это совсем другое...)

Это не является нарушением принципа заменяемости Лискова, но может запутать того, кто пытается использовать тип. (В зависимости от того, вызвали ли вы метод через тип Map или через тип MyHashMap, вы могли бы в итоге использовать разные методы с разной семантикой ....)

2
ответ дан 18 December 2019 в 05:43
поделиться

Существует очень большая проблема, а именно: она не позволяет «запускать программу мгновенно, а ЗАТЕМ проверять и загружать любые обновления в фоновом режиме» развертываний, к чему сходится поведение defacto приложений.

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

-121--1334877-

Это довольно просто, на самом деле: причина, почему $ SAFE не ведет себя так, как вы ожидаете от глобальной переменной, потому что это не глобальная переменная. Это волшебный единорог тингамаджигги.

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

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

$ SAFE имеет почти все вышеперечисленное: он является локальным потоком, что означает, что при его изменении в одном потоке он не влияет на другие потоки. Он является локальным, что означает, что при его изменении в локальной области (такой как класс, модуль, метод или блок) он не влияет на внешнюю область (как вы обнаружили). Он имеет магическое значение для интерпретатора, так как установка его в значение, отличное от 0 делает некоторые вещи не работают. И он имеет дополнительную странную семантику в том, что вы можете только когда-либо увеличить его значение, никогда уменьшить его.

-121--4195939-

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

Я думаю, что лучше было бы реализовать интерфейс Map и сохранить HashMap/HashTable как внутренний параметр объекта.

0
ответ дан 18 December 2019 в 05:43
поделиться
Другие вопросы по тегам:

Похожие вопросы: