На собеседованиях часто обсуждают методы
equals и hashcode. За что отвечают, как соотносятся между собой, когда переопределять, а когда не стоит.Если хочется посмотреть, как думает человек за пределами стандартного ответа, возможен такой диалог:
— Как считается хэшкод по умолчанию?
— Это адрес объекта в памяти
— А почему так?
— Адрес каждого объекта уникален, то что надо для хэшкода
— Сборщик мусора перемещает объекты внутри памяти. Как это влияет на значения хэшей?
— 😥
Тут можно предположить, что хэшкод считается один раз и приписывается к самому объекту. Это будет логичная мысль.
А вот вычисление хэша на основе адреса в памяти — популярный миф. В этом посте разберём, как на самом считается хэшкод под умолчанию.
В разных JVM реализации могут отличаться. Рассмотрим исходный код hashcode в OpenJDK. Там 6(!) стратегий вычисления хэшкода. Стратегия задаётся опциями VM:
-XX:+UnlockExperimentalVMOptions -XX:hashCode={число}При первом вызове хэш сохраняется внутри объекта и не меняется. Теперь к стратегиям:
🔸-XX:hashCode=0
Случайное число по алгоритму Lehmer RNG. Генератор один на всех, поэтому работает медленно
🔸-XX:hashCode=2
Чемпион по скорости, всегда возвращает 1:
java.lang.Object@1
Используется как отправная точка для тестов остальных стратегий
🔸-XX:hashCode=3
Обычная возрастающая последовательность:
java.lang.Object@a4
java.lang.Object@a5
java.lang.Object@a6
🔸-XX:hashCode=4
Текущий адрес в памяти. Популярный, но неправильный ответ на собеседованиях. Отчасти в этом виновата спецификация: там адрес приводится как пример реализации. Работает быстро, но не даёт равномерного распределения и должного уровня уникальности
🔸-XX:hashCode=1
Адрес объекта в памяти и немного манипуляций с битами
🔸 Стратегия по умолчанию
Случайное число по алгоритму Xorshift RNG. Следующее значение вычисляется на основе предыдущего. Значения равномерно распределены. Работает быстро, тк у каждого потока свой генератор, и синхронизации между потоками нет
Рейтинг стратегий по скорости:
🏆 Вернуть единицу: 184 операций за микросекунду
🥈 Вариант по умолчанию: 176 оп/мск
🥉 Адрес в памяти-1: 160 оп/мск
▪️ Растущая последовательность: 14 оп/мск
▪️ Случайное число и глобальная переменная: 10 оп/мск
Интересно, что до java 8 самая медленная опция была вариантом по умолчанию.
Итого
✅ Реализация хэшкода зависит от JVM и VM-флажков
✅ В OpenJDK 6 стратегий вычисления хэшкода. По умолчанию используется генератор случайных чисел в рамках одного потока
✅ Расчёт на основе адреса памяти не очень хорош по итоговым характеристикам
✅ Общие переменные в методах снижают производительность при интенсивном использовании. Яркие примеры — стратегии хэшкода с общим генератором и последовательностью