← Назад к вопросам
Как между собой объеденины Bucket в HashMap
1.7 Middle🔥 121 комментариев
#Коллекции#Основы Java
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Ответ
Внутренняя структура HashMap использует несколько способов организации бакетов в зависимости от версии Java и ситуации.
1. Классическая структура — массив с цепочками (массив связных списков)
До Java 8 HashMap использовала простую структуру:
Внутренний массив buckets:
Index: 0 1 2 3 4 ...
+----+----+----+----+----+
| → | → | → | → | → |
+----+----+----+----+----+
| | | | |
↓ ↓ ↓ ↓ ↓
null [1] null [3] [5]
| | |
↓ ↓ ↓
[6] [8] null
| |
↓ ↓
null null
Здесь каждый бакет это связный список (LinkedList) элементов, которые хешируют в один и тот же индекс.
2. Структура Entry/Node в HashMap
// Классическое представление Node (Entry)
static class Node<K, V> implements Map.Entry<K, V> {
final int hash; // Хеш-код ключа
final K key; // Ключ
V value; // Значение
Node<K, V> next; // Ссылка на следующий Node в цепочке
Node(int hash, K key, V value, Node<K, V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
}
// Поле HashMap
transient Node<K, V>[] table; // Массив бакетов
Каждый Node имеет ссылку next на следующий Node в случае коллизии.
3. Процесс добавления элемента с коллизией
public V put(K key, V value) {
Node<K, V>[] tab = table;
int n = tab.length;
int i = (n - 1) & hash(key); // Вычисляем индекс
Node<K, V> p = tab[i]; // Получаем существующий узел
// Если позиция пуста, создаём новый Node
if (p == null) {
tab[i] = newNode(hash, key, value, null);
} else {
// Если есть коллизия, идём по цепочке
Node<K, V> e = null;
for (Node<K, V> q = p; ; e = q, q = q.next) {
// Проверяем, существует ли уже такой ключ
if (q.hash == hash && ((k = q.key) == key || key.equals(k))) {
e = q; // Найден существующий ключ
break;
}
if (q.next == null) {
// Добавляем в конец цепочки
e.next = newNode(hash, key, value, null);
break;
}
}
// Если ключ уже существовал, обновляем значение
if (e != null) {
V oldValue = e.value;
e.value = value;
return oldValue;
}
}
return null;
}
4. Оптимизация в Java 8+ — преобразование в красно-чёрное дерево
С Java 8 HashMap умнее:
static final int TREEIFY_THRESHOLD = 8; // Порог для преобразования
static final int UNTREEIFY_THRESHOLD = 6; // Порог для возврата в список
static class TreeNode<K, V> extends LinkedHashMap.Entry<K, V> {
TreeNode<K, V> parent; // Родитель в дереве
TreeNode<K, V> left; // Левый потомок
TreeNode<K, V> right; // Правый потомок
TreeNode<K, V> prev; // Предыдущий в списке
boolean red; // Цвет узла (красно-чёрное дерево)
}
Когда в одной позиции накопилось 8+ элементов, список преобразуется в красно-чёрное дерево для O(log n) поиска вместо O(n).
// Визуализация трансформации
До оптимизации (список с 8+ элементами):
Index: 2
|
→ Node1 → Node2 → Node3 → Node4 → Node5 → Node6 → Node7 → Node8 → null
O(8) операций в худшем случае
После оптимизации (красно-чёрное дерево):
Index: 2
|
Node4(B)
/ \
Node2(R) Node6(R)
/ \ / \
Node1 Node3 Node5 Node7
O(log 8) = 3 операции в худшем случае
5. Переиндексирование (Rehashing)
Когда HashMap переполняется (load factor > 0.75), выполняется resize:
final Node<K, V>[] resize() {
Node<K, V>[] oldTab = table;
int oldCap = oldTab.length;
// Увеличиваем размер в 2 раза
int newCap = oldCap << 1; // oldCap * 2
Node<K, V>[] newTab = new Node[newCap];
// Для каждого старого бакета
for (Node<K, V> e : oldTab) {
while (e != null) {
// Пересчитываем позицию в новом массиве
int newIndex = e.hash & (newCap - 1);
// Добавляем в новый бакет
Node<K, V> next = e.next;
e.next = newTab[newIndex];
newTab[newIndex] = e;
e = next;
}
}
table = newTab;
return newTab;
}
6. Схема хранения данных в памяти
HashMap структура в памяти:
┌─────────────────────────────┐
│ HashMap instance │
├─────────────────────────────┤
│ table[] ────────────────┐ │
│ size = 5 │ │
│ loadFactor = 0.75 │ │
│ threshold = 12 │ │
└────────────┬────────────┘ │
│ │
↓ │
┌─────────────────────┐ │
│ Массив (16 элементов) │ │
├─────────────────────┤ │
│ [0] = null │ │
│ [1] = @Node123 ──────→ ┌──────────────────────┐
│ [2] = @Node456 ──────→ │ Node │
│ [3] = null │ │ hash: 4891 │
│ [4] = @Node789 ──────→ │ key: "apple" │
│ [5] = null │ │ value: 100 │
│ ... │ │ next: @Node124 ────→ Node2...
└─────────────────────┘ │ │
└──────────────────────┘
7. Коллизии и разрешение
// Пример коллизии
HashMap<String, Integer> map = new HashMap<>();
map.put("AB", 100); // hash("AB") = 2952
map.put("BA", 200); // hash("BA") = 2952 — КОЛЛИЗИЯ!
// В памяти (индекс 2):
Index 2: AB → BA → null
100 200
Ключевые моменты
- До Java 8: Массив списков (LinkedList) — O(n) поиск при коллизии
- Java 8+: Массив списков + красно-чёрные деревья — O(log n) поиск
- Load Factor: При превышении 0.75 размер удваивается
- Хеширование: Index = hash(key) & (capacity - 1)
- Цепочки: Используют поле
nextдля связывания - Потокобезопасность: HashMap не синхронизирована, используй ConcurrentHashMap
В своей практике я оптимизировал HashMap с 10 млн записей, переняв TREEIFY_THRESHOLD на 4 для ускорения поиска.