Как спроектировать хеш-ключ для составного объекта и почему важны equals/hashCode?
Стеки, очереди и хеш-таблицы
Хеш-таблица находит элемент по двум вопросам — «в какой корзине?» (hashCode) и «это тот же ключ?» (equals); если равные по смыслу объекты дают разные хеши или ключ мутирует после вставки, запись становится недоступной.
Контракт equals/hashCode
- Равные по
equalsобъекты обязаны иметь равныйhashCode; обратное не требуется. - Переопределили
equals, но неhashCodeв Java —map.getпо «равному» ключу вернётnull. - Ключ должен быть неизменяемым: после мутации хеш другой, поиск идёт не в ту корзину, а запись висит в таблице мёртвым грузом.
JS Map и объектные ключи
В JS нельзя переопределить равенство: Map сравнивает объекты по ссылке, поэтому map.get({ x: 1, y: 2 }) вернёт undefined. Варианты:
const grid = new Map(); // ключ "x:y" → число
const key = (x, y) => `${x}:${y}`;
grid.set(key(1, 2), 42);
grid.get(key(1, 2)); // 42
// Без разделителя ["1", "12"] и ["11", "2"] дали бы один ключ "112"
const tupleKey = (...parts) => JSON.stringify(parts);- Строковая сериализация: разделитель обязателен, а
JSON.stringifyобъекта зависит от порядка свойств — сериализуйте массив полей в фиксированном порядке. - Упаковка чисел:
x * WIDTH + yдля ограниченных целых — быстрее строк. - Вложенные
Map<x, Map<y, V>>— без сериализации вообще. - Интернирование: фабрика возвращает один и тот же объект для равных полей.
Коротко для собеседования
Таблица ищет корзину по hashCode, а сверяет ключ по equals, поэтому равные объекты обязаны давать одинаковый хеш, а ключ не должен меняться после вставки. В JS Map сравнивает объекты по ссылке, так что составной ключ сериализуют в строку с разделителем или упаковывают в число, а не прогоняют через JSON.stringify целиком.