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?