Что хранится в ячейке (bucket) массива внутри HashMap в Java?

Ответ

Каждая ячейка (bucket) внутреннего массива HashMap может содержать одну из следующих структур:

  1. null — ячейка пуста.
  2. Один узел Node<K,V> — если коллизий хеш-кодов нет.
  3. Связный список узлов Node<K,V> — при возникновении коллизии (разные ключи имеют одинаковый хеш для данного bucket).
  4. Красно-черное дерево TreeNode<K,V> (Java 8+) — если коллизий много (по умолчанию, когда список превышает 8 элементов), для обеспечения производительности O(log n).

Структура узла Node:

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash; // Хеш-код ключа
    final K key;    // Ключ
    V value;        // Значение
    Node<K,V> next; // Ссылка на следующий узел в списке/дереве
    // ... методы
}

Почему такая структура? Список обрабатывает редкие коллизии, а дерево оптимизирует поиск при их большом количестве, предотвращая деградацию производительности до O(n).

Ответ 18+ 🔞

А, слушай, вот эта вся кухня с HashMap — это же просто песня, блядь! Представь себе массив, обычный такой, прямолинейный. Каждая его ячейка — это как ведро, bucket, понимаешь? И вот что в этом ведре может валяться, аж четыре варианта, ёпта!

  1. null — абсолютная пустота, вакуум. Ни хуя нет, даже пылинки. Ведро свободно, можно заселять.
  2. Одинокий узел Node<K,V> — идеальный мир, коллизий нет. Зашёл один ключ, занял ведёрко, и всё, красота.
  3. Связный список этих самых Node — а вот тут начинается веселье, блядь. Коллизия случилась! Разные ключи, а хеш у них, сука, совпал для этого конкретного ведра. Ну, сели они тут все, как горох в стручке, друг за дружкой цепляются через next. Искать уже дольше, конечно.
  4. Красно-чёрное дерево TreeNode<K,V> — это уже, блядь, высший пилотаж, с Java 8. Когда разработчики увидели, что в каком-то ведре список разросся до 8 узлов, они такие: «Ну нахуй, хватит это терпеть!». И превращают этот бардак в упорядоченное дерево. Чтобы поиск был не за O(n), а за O(log n). Просто чтобы какой-нибудь зловредный ключ всех не положил.

А сам узел, этот кирпичик всего этого цирка, выглядит вот так, проще некуда:

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash; // Хеш ключа, раз и навсегда, блядь
    final K key;    // Ключ, тоже финальный, не переприсвоишь
    V value;        // А вот значение — пожалуйста, меняй на здоровье
    Node<K,V> next; // Ну и ссылочка на следующего бедолагу в этой цепочке или дереве
    // ... ну и методы там всякие
}

А зачем, спрашивается, этот геморрой со списком и деревом? Да затем, ёбана вошь, чтобы жизнь мёдом не казалась! Список — он для редких, случайных коллизий, с ними и так сойдёт. А дерево — это уже тяжёлая артиллерия на случай, если кто-то специально или случайно устроит атаку коллизиями. Чтобы наша мапа из шустрой O(1) не превратилась в ползучую O(n), где каждый поиск — это проход по всему длиннющему списку. Умные ребята подстраховались, вот и вся магия.