
HashMap
09.08.2020 | https://youtu.be/qlVF2RErvEU |
09.08.2020 | https://youtu.be/lH-Stxa8zQ8 |
09.12.2020 | https://youtu.be/j4C2o_j_y6U |
18.11.2011 | https://habr.com/ru/articles/132884/ |
07.08.2012 | https://habr.com/ru/articles/129037/ |
05.10.2011 | https://habr.com/ru/post/128017 |
24.08.2018 | https://habr.com/ru/post/421179 |
HashMap
Ассоциативный массив для хранения пар «ключ-значение». Работает на основе хэширования, быстро добавляет, удаляет и ищет данные. Не синхронизирован, поэтому не потокобезопасен.
• Использует хэш-таблицу для хранения данных. В случае коллизий элементы группируются в связные списки или деревья (с Java 8).
• Операции добавления, удаления, получения выполняются в среднем за O(1).
• Ключи могут быть null (только один null) и любыми объектами. Примитивы используются через классы-обертки (Integer, Double).
• Не реализует интерфейс Collection, но является частью Map.
• Не потокобезопасен. Одновременный доступ из нескольких потоков может привести к исключению ConcurrentModificationException. Используй ConcurrentHashMap или синхронизацию.
HashMap<String, Integer> map = new HashMap<>();Создание HashMap
Создается массив элементов (backet, корзина). Каждый элемент содержит LinkedList.
• initialCapacity - начальное количество сегментов в таблице (по умолчанию 16). является степенью 2. максимальный размер: Int / 2.
• loadFactor - коэффициент загрузки таблицы. используется для увеличения емкости.
• Когда table[] заполняется до предельного значения - его размер увеличивается вдвое и просиходит перераспределение элементов.
val map = HashMap<String, String>(
initialCapacity = 16,
loadFactor = 0.75F
)Добавление элемента
• В первую очередь key проверяется на null. Если key = null то элемент помешается в 0 корзину. Его хеш = 0.
• Для key вычисляется хешкод.
• Для хешкода вычисляется индекс корзины куда будет помещен элемент (0 - 15 для размера таблицы 16). если хешкоды равны то и индексы равны.
• Создается объект Node. он добавляется в корзину и становится первым элементом.
• Если при добавлении совпали индексы то элемент добавляется в корзину в конец LinkedList а существующий элемент начинает на него ссылаться.
• Если при добавлении совпали хешкоды то случается коллизия. новый элемент сравнивается поочередно со всеми элементами в корзине. если хешкоды одинаковые - элементы проверяются на равенство с помощью метода equals. Если false - элемент добавляется в конец LinkedList.
• Если при добавлении элемента совпали хешкоды и метод equals то элемент перезапишется в корзине.
data class Student(
val name: String
)
val map = HashMap<Student, String>()
map.put(Student("John"), "5.0")Получение элемента
• Сначала вычисляется хешкод для объекта key.
• Находится индекс корзины для хешкода ключа.
• Все элементы в LinkedList поочередно сравниваются по хешкоду с текущим. если хешкоды совпали - элементы сравниваются методом equals. Если true - элемент найден.
• Порядок элементов не гарантируется.
• Если у элемента который используется в качестве key в HashMap изменится поле, которое участвует в определении hashCode, то при обращении по key объект будет потерян.
• Если переопределить equals без hashCode или оба метода неправильно и эти объекты использовать в качестве key в HashMap (используя один объект как ключ, положить значение, используя другой – искать его), то значение найдено не будет.
val map = HashMap<Student, String>()
val value: String? = map.get(Student("John"))Удаление элемента
• При удалении элементов размер table[] остается прежним. размер HashMap уменьшается но не до предыдущего.
val map = HashMap<Student, String>()
map.remove(Student("John"))
val newMap = HashMap<Student, String>(map) // вернуться к прежнему размеру.Node
Внутренний класс HashMap. Его также называют Entry (имплементирует интерфейс Map.Entry).
• hash - хеш ключа.
• key - ключ.
• value - значение.
• next - ссылка на следующий элемент в LinkedList. Если нет - равен null.
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
}Преимущества HashMap
• Быстрая реализация. вместо перебора всех элементов HashMap точно значет где искать.
Сложность операций
Вставка, удаление и поиск элементов в HashMap обычно имеют среднюю временную сложность O(1). Однако, в случае множества коллизий, сложность может возрасти до O(n), где n — количество элементов в корзине.
synchronized HashMap
Потокобезопасная версия HashMap. синхронизирует доступ к методам мапы, позволяя безопасно работать с ней в многопоточной среде.
Map<String, Integer> syncMap = Collections.synchronizedMap(new HashMap<>());LinkedHashMap
Класс в Java, который наследует HashMap, но дополнительно сохраняет порядок вставки или порядок доступа к элементам. Это делает его удобным для задач, где важно сохранять последовательность элементов.
LinkedHashMap<String, Integer> map = new LinkedHashMap<>(/*initialCapacity*/ 16, /*loadFactor*/ 0.75F, /*accessOrder*/ false);accessOrder
Задает порядок элементов. При значении false используется порядок вставки. При true порядок меняется при обращении к элементу. Полезно для реализации LRU-кэшей.
ConcurrentHashMap
Потокобезопасная реализация интерфейса Map, которая используется для управления коллекциями ключ-значение в многопоточных средах. В отличие от обычного HashMap, который не является потокобезопасным, ConcurrentHashMap обеспечивает синхронизацию и управление доступом к данным, что позволяет нескольким потокам одновременно безопасно читать и изменять содержимое мапы.
• Благодаря сегментированию при изменении элемента блокируется только сегмент в котором он находится.
• key и value не могут быть null.
• Под капотом все поля объявлены как volatile.
val map = ConcurrentHashMap<String, Int>()putIfAbsent
Атомарная операция. Добавляет значение только в том случае, если указанный ключ отсутствует в карте.
map.putIfAbsent("three", 3)computeIfPresent
Атомарная операция. Выполняет вычисление и обновляет значение только если ключ уже существует.
map.computeIfPresent("three") { _, value -> value + 1 }replace
Атомарная операция. Позволяет заменить значение, связанное с указанным ключом, только если этот ключ уже существует в карте.
• Замена значения по ключу
val map = ConcurrentHashMap<String, Int>()
map["count"] = 5
val previousValue = map.replace("count", 10)• Замена значения, если текущее значение совпадает с указанным
val map = ConcurrentHashMap<String, Int>()
map["value"] = 20
val isReplaced = map.replace("value", 20, 30)Вопросы на собесе (23)
Key (5)
- Какие условия у ключа в HashMap?
Ключи в
HashMapдолжны правильно переопределять методыequalsиhashCodeдля корректной работы.
- Если ключ null - где хранится значение? В какой бакет попадает?
Первый бакет. Позиция с индексом 0.
- Что происходит при добавлении элемента с существующим ключом в HashMap?
Если при добавлении совпали хешкоды то случается коллизия. новый элемент сравнивается поочередно со всеми элементами в корзине. если хешкоды одинаковые - элементы проверяются на равенство с помощью метода
equals. Если false - элемент добавляется в конецLinkedList.
- Хотим использовать Java-класс как ключ в HashMap, что нужно переопределить у класса?
Методы
equals()иhashCode().
- data-class может быть ключом?
Да. При объявлении
data-класса автоматически генерируются методыequals()иhashCode(), которые необходимы для корректной работы с ключами вHashMap.
- Какие условия у ключа в HashMap?
Bucket (3)
- Как определяется Bucket?
По hashCode, через операцию взятия остатка от деления (модуль).
- Как объекты хранятся внутри бакета?
Внутри бакета объекты хранятся в виде цепочки (связанного списка) или сбалансированного дерева (если количество объектов в бакете превышает порог). Каждый элемент в бакете представлен в виде пары ключ-значение, где ключ используется для поиска соответствующего значения.
- Для чего нужен связный список в бакете?
Связный список используется для обработки коллизий. Когда несколько элементов имеют одинаковый хэш-код и попадают в один и тот же бакет, они сохраняются в виде связного списка, что позволяет эффективно хранить и извлекать элементы, обеспечивая при этом доступ к ним за O(n) в случае коллизий, где n — количество элементов в этом бакете.
- Как определяется Bucket?
ConcurrentHashMap (2)
- Для чего используется ConcurrentHashMap?
ConcurrentHashMapиспользуется для потокобезопасного хранения и управления коллекцией ключ-значение в многопоточной среде, позволяя нескольким потокам одновременно читать и изменять данные без полной блокировки всей мапы. Он обеспечивает высокую производительность благодаря частичной блокировке и атомарным операциям.
- Как ConcurrentHashMap устроена под капотом?
ConcurrentHashMapразбивает данные на сегменты, что позволяет нескольким потокам одновременно модифицировать разные части мапы, минимизируя блокировки и повышая производительность. Она использует CAS (Compare-And-Swap) и частичные блокировки для атомарного выполнения операций и управления конкурентным доступом.
- Для чего используется ConcurrentHashMap?
Другие (13)
- Как устроен HashMap?
HashMapхранит данные в виде пар ключ-значение, используя хэширование для быстрого поиска. Ключи хэшируются для определения бакета, в котором хранится пара, а при коллизии ключей используется связанный список или дерево (в зависимости от количества элементов в бакете). Это обеспечивает быстрый доступ к элементам, обычно с временной сложностью O(1), но может ухудшиться до O(n) при частых коллизиях.
- Какой контракт между equals и hashCode в HashMap?
• Равные объекты имели одинаковый
hashCode: если два объектаaиbравны (a.equals(b) == true), то их хэш-коды должны быть одинаковыми (a.hashCode() == b.hashCode()). Это позволяетHashMapнаходить правильную ячейку для хранения и поиска объекта.• Неравные объекты могут иметь разные или одинаковые
hashCode: еслиa.equals(b) == false, ихhashCodeможет быть разным или одинаковым. Одинаковый хэш-код для неравных объектов возможен, но это приведёт к так называемым коллизиям вHashMap. В случае коллизийHashMapбудет использовать методequalsдля точного сравнения объектов в одном бакете.
- Если у 2 объектов одинаковые хэш-коды значит ли это что их equals = true?
Нет, два объекта могут иметь одинаковый
hashCode, но при этом быть неравными с точки зренияequals. Это называется коллизией хэш-кодов.
- Почему время доступа к HashMap падает при большом количестве коллизий, но не становится O(n)?
При значительном увеличении числа коллизий, HashMap преобразует списки в бакетах в сбалансированные деревья. Это улучшает время доступа до O(log n) в случае коллизий, что предотвращает ухудшение производительности до O(n).
- Для чего нужен loadFactor в HashMap?
Используется для определения, когда нужно увеличить размер массива бакетов, чтобы сохранить эффективность поиска и уменьшить количество коллизий. Он представляет собой отношение количества элементов в
HashMapк числу бакетов, при превышении которого происходит увеличение ёмкостиHashMap.
- Для чего нужен initial capacity в HashMap?
Определяет начальный размер массива для хранения элементов и уменьшает количество перераспределений.
- Что такое коллизия в HashMap?
Если в
HashMapдобавить два элемента с одинаковым хэш-кодом, они попадут в один и тот же бакет. Однако, если ключи при этом разные (неравны по методуequals), оба элемента будут храниться в этом бакете в виде цепочки. Если ключи одинаковы, второй элемент заменит первый.
- Может ли элемент в HashMap потеряться?
Да. Если изменится
hashCode-HashMapне сможет найти объект по ключу. Если реализацияequals()иhashCode()некорректная - это может привести к неожиданному поведению и потерей элементов, так какHashMapполагается на эти методы для сравнения ключей.
- Как специально потерять объект в HashMap?
Изменить его
hashCode.
- Что произойдет при добавлении в HashMap 2 разных объектов c одинаковым хэш-кодом?
Если объекты равны по equals - они будут размещены в одном бакете, иначе один объект перезапишет второй. HashMap разрешит коллизию, добавив второй объект в список (или дерево, если элементов много) в этом бакете, и при поиске будет использовать equals для точного соответствия.
- Какой пошаговый алгоритм добавления объекта в HashMap?
• Проверить
keyнаnull. Еслиkey= null то элемент помешается в 0 корзину. Его хеш = 0.• Для
keyвычисляется хешкод.• Для хешкода вычисляется номер бакета куда будет помещен элемент (0 - 15 для размера таблицы 16). Если хешкоды равны то и индексы равны.
• Создается объект
Node. Он добавляется в корзину и становится первым элементом.• Если при добавлении совпали индексы то элемент добавляется в корзину в конец
LinkedListа существующий элемент начинает на него ссылаться.• Если при добавлении совпали хешкоды то случается коллизия. Новый элемент сравнивается поочередно со всеми элементами в корзине. Если хешкоды одинаковые - элементы проверяются на равенство с помощью метода
equals. Если false - элемент добавляется в конецLinkedList.• Если при добавлении элемента совпали хешкоды и метод
equalsто элемент перезапишется в корзине.
- Какой пошаговый алгоритм получения объекта из HashMap?
• Вычисление хэш-кода: Вызывается
hashCode()для ключа.• Определение индекса: Хэш-код преобразуется в индекс массива бакетов.
• Поиск в бакете: Если бакет пустой, возвращается null. Если нет, перебираются элементы.
• Сравнение ключей: Сравниваются хэш-коды и ключи с помощью
equals().• Возврат значения: Возвращается соответствующее значение или null, если ключ не найден.
- Какова скорость базовых операций у HashMap?
O(1). При коллизиях O(n).
- Как устроен HashMap?