По какому принципу выбирается бакет в HashMap для хранения ключа и как вычисляется индекс бакета
Что проверяют
Заглядывали ли вы в исходники HashMap или знаете схему по пересказам. По ответу видно, понимаете ли вы, зачем хеш дополнительно перемешивают и почему размер таблицы всегда степень двойки.
Как отвечать
У ключа берётся hashCode(), но напрямую он не используется. HashMap считает свой хеш:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}Индекс бакета: i = (n - 1) & hash, где n это длина массива. Так как n степень двойки, побитовое И работает как остаток от деления, но дешевле. Расплата в том, что берутся только младшие биты, поэтому сдвиг с xor и подмешивает в них старшие: без этого ключи, различающиеся лишь в старшей половине, легли бы в один бакет. Ключ null всегда попадает в нулевой бакет.
Дальше про коллизии: в бакете список узлов, при восьми узлах и таблице от 64 ячеек список превращается в красно-чёрное дерево, при сжатии до шести возвращается обратно. Расширение идёт, когда число элементов превышает произведение ёмкости на коэффициент загрузки (по умолчанию 0.75), таблица удваивается, и при переносе элемент либо остаётся на месте, либо уезжает на index + oldCap, потому что маска удлинилась ровно на один бит.
Где спотыкаются
Говорят, что h ^ (h >>> 16) нужен, чтобы сделать хеш положительным. Ничего он не выпрямляет: знаковый бит срезает маска (n - 1) &, а сдвиг решает другую задачу, подмешивает старшие биты в младшие.
Используют изменяемый объект как ключ. Положили, поменяли поле, участвующее в hashCode, и значение больше не находится, хотя физически лежит в мапе и видно в отладчике.
Переопределяют equals без hashCode или возвращают константу. Все ключи собираются в один бакет, и поиск вырождается в перебор; дерево спасает лишь частично, оно требует, чтобы ключи были сравнимы.