После достижения какой длины список превращается в дерево в HashMap
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
После достижения какой длины список превращается в дерево в HashMap
Краткий ответ
В Java 8 и позже, когда в корзине (bucket) HashMap накапливается 8 элементов и размер таблицы >= 64, список преобразуется в красно-черное дерево (Red-Black Tree). Обратное преобразование происходит при 6 элементах.
История эволюции
Java 7 и ранее
// До Java 8
// HashMap использовал связные списки (linked lists) для разрешения коллизий
// Структура корзины:
// Bucket[0] → Node → Node → Node → Node → ... (O(n) в худшем случае)
// Проблема: При плохом hash code все элементы могут попасть в одну корзину
HashMap<String, String> map = new HashMap<>();
for (int i = 0; i < 1000; i++) {
// Если все ключи имеют одинаковый hash код
// то все 1000 элементов окажутся в одной цепочке
// Поиск будет O(n) вместо O(1)
}
Java 8+
Джава 8 предоставила решение: гибридная структура
Java 8+ HashMap Bucket Structure:
Если elements < TREEIFY_THRESHOLD (8):
Bucket → Linked List (Node → Node → Node)
Complexity: O(n) для поиска
Если elements >= TREEIFY_THRESHOLD (8) И table.length >= MIN_TREEIFY_CAPACITY (64):
Bucket → Red-Black Tree (TreeNode)
Complexity: O(log n) для поиска
Обратное преобразование при UNTREEIFY_THRESHOLD (6):
Bucket → Linked List снова
(Удаление элементов → размер < 6 → back to linked list)
Константы в HashMap коде
public class HashMap<K, V> {
// Максимальная длина списка перед преобразованием в дерево
static final int TREEIFY_THRESHOLD = 8;
// Минимальный размер таблицы для преобразования в дерево
static final int MIN_TREEIFY_CAPACITY = 64;
// При удалении элементов дерево превращается в список
static final int UNTREEIFY_THRESHOLD = 6;
// Красно-черное дерево
static final 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; // цвет узла (красный/черный)
// методы для балансировки дерева...
}
}
Когда происходит преобразование
Сценарий 1: При добавлении 8-го элемента в одну корзину
HashMap<String, Integer> map = new HashMap<>();
// Предположим, все элементы имеют одинаковый hash код
// (упрощено для примера)
map.put("key1", 1); // Bucket[0]: Node1
map.put("key2", 2); // Bucket[0]: Node1 → Node2
map.put("key3", 3); // Bucket[0]: Node1 → Node2 → Node3
map.put("key4", 4); // Bucket[0]: Node1 → Node2 → Node3 → Node4
map.put("key5", 5); // Bucket[0]: Node1 → ... → Node5 (size=5, linked list)
map.put("key6", 6); // Bucket[0]: Node1 → ... → Node6 (size=6, still linked list)
map.put("key7", 7); // Bucket[0]: Node1 → ... → Node7 (size=7, still linked list)
map.put("key8", 8); // Bucket[0]: Node1 → ... → Node8 (size=8)
// TREEIFY_THRESHOLD достигнут!
// Проверка: table.length >= 64?
// Если ДА → преобразуй в Red-Black Tree
// Если НЕТ → осталось linked list, но подумай о resize
Сценарий 2: При resize() таблицы
// Когда HashMap растет (resize), она перехешует все элементы
// Это может привести к возникновению длинных цепочек в новой таблице
HashMap<String, Integer> map = new HashMap<>();
// Добавим элементы...
for (int i = 0; i < 100; i++) {
map.put("key" + i, i);
}
// HashMap будет resize несколько раз:
// capacity: 16 → 32 → 64 → 128 → ...
// Когда capacity становится >= 64, AND
// какой-то bucket имеет >= 8 элементов,
// то этот bucket преобразуется в дерево
Полный процесс treeify
// Упрощенный код из HashMap
public V put(K key, V value) {
// ... find bucket ...
if (binCount >= TREEIFY_THRESHOLD - 1) { // -1 потому что еще не добавлен
treeifyBin(tab, hash);
}
return putVal(hash, key, value, false, true);
}
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n = tab.length;
// Условие 1: Таблица должна быть достаточно большой
if (n < MIN_TREEIFY_CAPACITY) {
resize(); // Если таблица мала, resize вместо treeify
return;
}
// Условие 2: Преобразуй связный список в красно-черное дерево
TreeNode<K,V> hd = null, tl = null;
for (Node<K,V> e = tab[hash & (n - 1)]; e != null; e = e.next) {
TreeNode<K,V> p = new TreeNode<K,V>(
e.hash, e.key, e.value, null
);
if (tl == null) {
hd = p; // голова дерева
} else {
// связываем в дерево
tl.prev = p;
p.next = tl;
}
tl = p;
}
// Встави дерево в таблицу
if ((tab[hash & (n - 1)] = hd) != null) {
hd.treeify(tab); // Балансируй дерево
}
}
Пример в коде: Когда случается преобразование
public class HashMapTreeifyExample {
public static void main(String[] args) {
HashMap<Integer, String> map = new HashMap<>();
// Сценарий: Добавляем элементы, которые хешируются в одну позицию
// Это сложно сделать с обычными Integer ключами, но вот пример
// с кастомным hash code:
class BadKey {
int value;
BadKey(int v) { this.value = v; }
@Override
public int hashCode() {
return 0; // ВСЕ элементы имеют одинаковый хеш!
}
@Override
public boolean equals(Object o) {
return o instanceof BadKey &&
((BadKey)o).value == this.value;
}
}
HashMap<BadKey, String> badMap = new HashMap<>();
// Добавим 8 элементов (все с hash code = 0)
for (int i = 0; i < 8; i++) {
badMap.put(new BadKey(i), "value" + i);
}
// После 8-го элемента (если capacity >= 64)
// корзина преобразуется из linked list в red-black tree
// Вместо O(n) поиск становится O(log n)
}
}
Почему красно-черное дерево?
Red-Black Tree выбрано потому что:
- Самобалансирующееся — высота O(log n)
- Гарантированная производительность — O(log n) поиск в худшем случае
- Хорошее соотношение между скоростью вставки/удаления и поиском
- Не требует полной перебалансировки как AVL дерево
Сравнение Bucket структур:
1. Linked List (Java 7):
- Insert: O(1)
- Search: O(n) ← ПЛОХО при коллизиях
- Delete: O(n)
2. Red-Black Tree (Java 8+):
- Insert: O(log n)
- Search: O(log n) ← ХОРОШО при коллизиях
- Delete: O(log n)
3. Гибрид (Java 8+):
- Малые bucket'ы: linked list (O(1) для поиска среди 5-6 элементов)
- Большие bucket'ы: tree (O(log n) для поиска среди 1000+ элементов)
Практический пример воздействия
public class HashMapPerformance {
// Симуляция плохого хеширования
static class Integer8 {
final int value;
Integer8(int v) { this.value = v; }
@Override public int hashCode() {
return value & 7; // только 8 возможных хешей (0-7)
}
@Override public boolean equals(Object o) {
return o instanceof Integer8 && ((Integer8)o).value == this.value;
}
}
public static void main(String[] args) {
HashMap<Integer8, String> map = new HashMap<>();
// Добавим 10_000 элементов
// Они распределятся по 8 bucket'ам (из-за плохого хеша)
// Каждый bucket получит ~1250 элементов
long start = System.nanoTime();
for (int i = 0; i < 10_000; i++) {
map.put(new Integer8(i), "value" + i);
}
// Каждый bucket с 1250 элементами:
// - С linked list: O(1250) в среднем при поиске
// - С red-black tree: O(log 1250) ≈ O(10) при поиске
//
// Улучшение: 1250 / 10 = 125x быстрее!
long end = System.nanoTime();
System.out.println("Time: " + (end - start) / 1_000_000 + "ms");
// Теперь поиск:
start = System.nanoTime();
for (int i = 0; i < 10_000; i++) {
map.get(new Integer8(i));
}
end = System.nanoTime();
System.out.println("Get time: " + (end - start) / 1_000_000 + "ms");
}
}
Вывод
При достижении 8 элементов в корзине HashMap преобразуется в красно-черное дерево (при условии, что размер таблицы >= 64). Это улучшение из Java 8 защищает от degenerate cases плохого хеширования, гарантируя O(log n) операции вместо O(n). Обратное преобразование происходит при 6 элементах для избежания постоянного преобразования на границе.
Это классический пример практической оптимизации: гибридный подход (linked list для малых bucket'ов, tree для больших) дает лучшую производительность в большинстве случаев.