• ,

9 главных вопросов о Map в Java


Оригинал
Напомним, что Map — это структурированные данные, состоящие из набора пар ключ-значение, и каждый ключ может использоваться только один раз в одной Map. Эта тема раскрывает 9 основных вопросов об использовании Map в Java и её воплощённых (имплементированных) классах. Для простоты, я буду использовать в примерах обобщения. Потому, я буду писать просто Map, не конкретизируя спецификатор Map. Но вы можете полагать, что оба значения K и V сопоставимы, что означает K расширяет Comparable и V так же расширяет Comparable.

0. Обращение Map в List
В Java, интерфейс Map предлагает три вида коллекций: набор ключей, набор значений и набор ключ-значение. Все они могут быть обращены в List при помощи конструктора или метода addAll(). Следующая вырезка кода демонстрирует как сделать ArrayList из Map.
//лист ключей
List keyList = new ArrayList(Map.keySet());
//лист значений
List valueList = new ArrayList(Map.valueSet());
//лист ключ-значения
List entryList = new ArrayList(Map.entrySet());


1. Пройтись по всем значениям в Map.
Проход по каждой паре ключ-значение — самая базовая, основная процедура прохода по Map. В Java, каждая пара хранится в поле Map называемом Map.Entry. Map.entrySet() возвращает набор ключ-значений, потому самым эффективным способом пройтись по всем значениям Map будет:
for(Entry entry: Map.entrySet()) {
  //получить ключ
  K key = entry.getKey();
  //получить значение
  V value = entry.getValue();
}

Так же мы можем использовать Iterator, особенно в версиях младше JDK 1.5
Iterator itr = Map.entrySet().iterator();
while(itr.hasNext()) {
  Entry entry = itr.next();
  //получить ключ
  K key = entry.getKey();
  //получить значение
  V value = entry.getValue();
}


2. Упорядочивание Map по ключам
Упорядочивание Map по ключам ещё одна часто встречаемая процедура. Первый способ: добавить Map.Entry в список, и упорядочить с использованием компаратора, что сортирует по значениям.
List list = new ArrayList(Map.entrySet());
Collections.sort(list, new Comparator() {
 
  @Override
  public int compare(Entry e1, Entry e2) {
    return e1.getKey().compareTo(e2.getKey());
  }
 
});

Другой способ: использовать SortedMap, которая ко всему, ещё и выстраивает свои ключи по порядку. Но, все ключи при этом должны воплощать Comparable или приниматься компаратором.
Один из имплементированных классов SortedMap — TreeMap. Её конструктор принимает компаратор. Следующий код показывает как превратить обычную Map в упорядоченную.
SortedMap sortedMap = new TreeMap(new Comparator() {
 
  @Override
  public int compare(K k1, K k2) {
    return k1.compareTo(k2);
  }
 
});
sortedMap.putAll(Map);


3. Упорядочивание Map по значениям
Добавление Map в список и последующая сортировка работают и в данном случае, но нужно в этот раз брать Entry.getValue(). Код ниже почти такой же как и раньше.
List list = new ArrayList(Map.entrySet());
Collections.sort(list, new Comparator() {
 
  @Override
  public int compare(Entry e1, Entry e2) {
    return e1.getValue().compareTo(e2.getValue());
  }
 
});

Мы всё ещё можем использовать SortedMap в данном случае, но только если значения уникальны. В таком случае, вы можете обратить пару ключ-значение в значение-ключ. Это решение обладает строгим ограничением, и не рекомендуется мною.

4. Инициализация статической/неизменной Map
Когда вы желаете, что бы Map оставалась неизменной, хорошим способом будет скопировать оную в неизменяемую (immutable) Map. Такая защитная техника программирования поможет вам создать не только безопасную для использования, но и так же потокобезопасную Map.
Для инициализации статической/неизменной Map, мы можем использовать инициализатор static (см. ниже). Проблема данного кода в том, что не смотря на объявление Map как static final, мы всё ещё можем работать с ней после инициализации, например Test.Map.put(3,«three»);. Так что это не настоящая неизменность.
Для создания неизменяемой Map с использованием статического инициализатора, нам нужен супер анонимный класс, который мы добавим в неизменяемую Map на последнем шаге инициализации. Пожалуйста, посмотрите на вторую часть кода. Когда будет выброшено UnsupportedOperationException, если вы запустите Test.Map.put(3,«three»);
public class Test {
 
  private static final Map Map;
  static {
    Map = new HashMap();
    Map.put(1, "one");
    Map.put(2, "two");
  }
}
public class Test {
 
  private static final Map Map;
  static {
    Map aMap = new HashMap();
    aMap.put(1, "one");
    aMap.put(2, "two");
    Map = Collections.unmodifiableMap(aMap);
  }
}

Библиотека Guava так же поддерживает различные способы инициализации статических и неизменных коллекций. Чтобы изучить подробнее преимущества утилиты Guava для неизменных коллекций, обратитесь к разделу Неизменные коллекции в Инструкции Guava.

5. Разница между HashMap, TreeMap, и Hashtable
Есть три основных воплощения интерфейса Map в Java: HashMap, TreeMap, и Hashtable. Главные отличия заключаются в следующем:
  • Порядок прохода. HashMap и HashTable не дают гарантий по упорядоченности в Map; в частности, они не гарантируют что порядок останется тем же самым в течении времени. Но TreeMap будет упорядочивать все значения в «естественном порядке» ключей или по компаратору.
  • Допустимые пары ключ-значение. HashMap позволяет иметь ключ null и значение null. HashTable не позволяет ключ null или значение null. Если TreeMap использует естественный порядок или компаратор не позволяет использовать ключ null, будет выброшено исключение.
  • Синхронизация. Только HashTable синхронизирована, остальные — нет. Но, «если потокобезопасное воплощение не нужно, рекомендуется использовать HashMap вместо HashTable».
Более подробное сравнение
.                      | HashMap | HashTable  | TreeMap
-------------------------------------------------------

Упорядочивание          |нет      |нет        | да
null в ключ-значение    | да-да   | нет-нет     | нет-да
синхронизировано        | нет     | да        | нет
производительность      | O(1)    | O(1)      | O(log n)
воплощение             | корзины | корзины   | красно-чёрное дерево

Прочтите подробнее об отношениях HashMap vs. TreeMap vs. Hashtable vs. LinkedHashMap.

6. Map с реверсивным поиском/просмотром
Иногда, нам нужен набор пар ключ-ключ, что подразумевает значения так же уникальны, как и ключи (паттерн один-к-одному). Такое постоянство позволяет создать «инвертированный просмотр/поиск» по Map. То есть, мы можем найти ключ по его значению. Такая структура данных называется двунаправленная Map, которая к сожалению, не поддерживается JDK.
Обе Apache Common Collections и Guava предлагают воплощение двунаправленной Map, называемые BidiMap и BiMap, соответственно. Обе вводят ограничение, которое задаёт соответствие 1:1 между ключами и значениями.

7. Поверхностная копия Map
Почти все, если не все, Map в Java содержат конструктор копирования другой Map. Но процедура копирования не синхронизирована. Что означает когда один поток копирует Map, другой может изменить её структуру. Для предотвращения внезапной рассинхронизации копирования, один из них должен использовать в таком случае Collections.synchronizedMap().
Map copiedMap = Collections.synchronizedMap(Map);

Другой интересный способ поверхностного копирования — использование метода clone(). Но он НЕ рекомендуется даже создателем фреймворка коллекций Java, Джоном Блохом. В споре "Конструктор копирования против клонирования", он занимает позицию:
Цитата:
«Я часто привожу публичный метод clone в конкретных классах, поскольку люди ожидают их там увидеть.… это позор, что Клонирование сломано, но это случилось.… Клонирование это слабое место, и я думаю люди должны быть предупреждены о его ограничениях.»
По этой причине, я даже не показываю вам, как использовать метод clone() для копирования Map

8. Создание пустой Map
Если Map неизменна, используйте
Map = Collections.emptyMap();

Или, используйте любое другое воплощение. Например
Map = new HashMap();

КОНЕЦ

12 комментариев

tanzwud
Спасибо.
Как замечание в тексте помоему есть неточность.
HashTable null в ключ-значение
помоему должно быть
нет — нет
Иначе будут nullpoint exceptions
Хотя могу быть не прав.
Treefeed
Действительно, тут у автора ошибка. Хотя сам выше пишет — HashTable does not allow null key or null values. Исправил.
Izuver
  • Izuver
  • 0
  • Комментарий отредактирован 2014-02-11 21:30:32 пользователем Izuver
Может, стоить переводить слово implementation как реализация, а не воплощение, а то как-то коряво смотрится. Также можно слово sort переводить как сортировка, чтобы было более привычно, вместо упорядочивания.
KeLsTaR
Один из имплементированных классов SortedMap — TreeMap. Её конструктор принимает компаратор.
Вот тут по-моему не верно. В смысле понятно что её — карты, но все же его — класса, его — структуры. Поэтому ожидалось увидеть слова «его», а слово «её» меня поначалу даже сбило.

И так же я оказался удивлен не увидев в этой статье одной изюминки, о которой обычно все время забывают (и тут забыли)). В общем вот тут сказано
Один из имплементированных классов SortedMap — TreeMap.
Это утверждение абсолютно верно, но вот если говорить о том, что имплементирует TreeMap, то заглянув в исходники вы увидите, что TreeMap, Разумеется, имплиментирует SortedMap, однако косвенно, так как напрямую TreeMap имплементирует NavigableMap, который уже в свою очередь наследует SortedMap.
Это важно не только из-за наследования, а еще и потому, что в NavigableMap есть очень хорошие методы типо lowerKey, lowerEntry, higherKey, higherEntry, firstEntry, lastEntry и другие. То есть если сделать
Map map = new TreeMap();
То воспользоваться этими методами вы не сможете, поэтому это было бы неплохо знать :)
FedoraLinux
Б><*ь, а я сижу, перевожу ее…
Вставлю пару замечаний.
1. Нехорошо писать следующим образом:
List keyList = new ArrayList(Map.keySet());

Это может привести к ошибкам. Автор указал List (да и другие коллекции в других примерах), не указав обобщенный тип коллекции. Так делать не стоит, ибо могут возникать ошибки. Если Вы не знаете заранее, какой тип данных будет хранить та или иная коллекция, то возможно, что лучшим выходом будет использование один из следующих вариантов (List взят как пример):
List<Object> entryList = new ArrayList<Object>();
// что будет эквивалентно
List<?> entryList = new ArrayList<?>();


2. Когда переводил, хотел добавить пример преобразования Map в List с использованием метода addAll(), но у меня не получилось никак его использовать для этого, да и нагуглить ничего подобного не получилось. Если кто покажет пример — было бы очень хорошо, но пока, сдается мне, автор это придумал.
bro
  • bro
  • 0
а можно ли добавить в map несколько, ну скажем, 10 пар ключ-значение, сразу, одной строкой?
Joysi
  • Joysi
  • +2
  • Комментарий отредактирован 2016-09-06 10:06:19 пользователем Joysi
Map<Integer, String> intToString = new HashMap<Integer, String>(){{
         put(1, "one");
         put(2, "two");
         put(3, "three");         
 }};

Что не совсем хорошо

info.javarush.ru/Joysi/2016/02/04/Double-brace-%D0%B8%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D1%8F.html
bro
будем НЕ использовать. просто, складно легко запомнить. спасибо.
Wardeng
в пункте 1 какая-то ошибка код не работает
for(Entry entry: Map.entrySet()) {

Нужно нечто вроде этого:
for (Map.Entry<String, String> x: map.entrySet()) {}
gal20040
  • gal20040
  • 0
  • Комментарий отредактирован 2017-12-16 23:19:03 пользователем gal20040
В чём выгода объявлять переменную типа Map, но инициализировать её типом HashMap, например?
Map map = new HashMap();
Почему сразу не обозначить её так:
HashMap map = new HashMap();
gal20040
Как обычно: главное правильно сформулировать вопрос.
Ответ гуглится после этого достаточно быстро: использование интерфейсов вместо классов.
fatfaggy
производительность O(1)
че, правда? как у массива?)
или автор имел ввиду производительность какой-то одной конкретной операции только?
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.