Разработка метода hashCode Java

Я изучаю Правило 9, Эффективная Java [Всегда переопределяйте hashcode (), когда переопределяете равно].

У меня есть несколько вопросов по поводу замечаний автора:

  1. Автор говорит:

На шаге 1 используется ненулевое начальное значение, поэтому на хеш-значение будут влиять начальные поля, хеш-значение которых, вычисленное на шаге 2.a, равно нулю. Если бы ноль использовался в качестве начального значения на шаге 1, на общее хеш-значение не повлияли бы какие-либо такие начальные поля, что могло бы увеличить коллизии. Значение 17 произвольно.

Шаг 2.а:

Для каждого значимого поля f в вашем объекте (то есть каждого поля, учитываемого методом equals) выполните следующие действия: a. Вычислите хэш-код int c для поля:

я. Если поле является логическим, вычислить (f? 1: 0).

II. Если поле является байтовым, символьным, коротким или целым, compute (int) f.

iii. Если поле длинное, вычислите (int) (f ^ (f ››› 32)).

iv. Если поле является плавающим, вычислите Float.floatToIntBits (f).

v. Если поле является двойным, вычислите Double.doubleToLongBits (f), а затем хешируйте полученную длину, как на шаге 2.a.iii.

vi. Если поле является ссылкой на объект и метод equals этого класса сравнивает поле, рекурсивно вызывая equals, рекурсивно вызывает hashCode для поля. Если требуется более сложное сравнение, вычислите «каноническое представление» для этого поля и вызовите hashCode для канонического представления. Если значение поля равно нулю, верните 0 (или другую константу, но обычно 0).

vii. Если поле является массивом, относитесь к нему так, как если бы каждый элемент был отдельным полем. То есть вычислить хэш-код для каждого значимого элемента, рекурсивно применяя эти правила, и объединить эти значения на шаге 2.b. Если каждый элемент в поле массива имеет значение, вы можете использовать один из методов Arrays.hashCode, добавленных в версии 1.5.

Предположим, результат рассчитывается как:

result = 31 * result + areaCode;      
result = 31 * result + prefix;
result = 31 * result + lineNumber;

В случае, если начальное значение результата равно 0, а все указанные выше поля равны 0, результат останется 0. Но, даже если результат изначально не равен 0, результат будет равняться одной и той же константе каждый раз, когда начальные поля равны 0, что будет : 31 * (31 * (31 * 17)). Как это значение поможет уменьшить количество столкновений?

  1. В последнем абзаце говорится, что:

Многие классы в библиотеках платформы Java, такие как String, Integer и Date, включают в свои спецификации точное значение, возвращаемое их методом hashCode как функцию значения экземпляра. Как правило, это не очень хорошая идея, так как это сильно ограничивает ваши возможности по улучшению хеш-функции в будущих выпусках. Если вы оставите детали хэш-функции неуказанными и будет обнаружен недостаток или обнаружена лучшая хеш-функция, вы можете изменить хеш-функцию в следующем выпуске, будучи уверенным, что никакие клиенты не зависят от точных значений, возвращаемых хеш-функцией.

Что он имеет в виду, говоря, что точное значение, возвращаемое hashCode, является функцией значения экземпляра?

Заранее благодарю за любую помощь.


person gaurav jain    schedule 01.12.2015    source источник
comment
По моему мнению, метод hashCode как функция значения экземпляра означает, что сгенерированный хэш-код будет зависеть от значений переменных объекта или экземпляра. Таким образом, возможно, что два объекта, имеющие одинаковые значения, могут генерировать один и тот же хэш-код. Что может привести к столкновению. Затем необходимо реализовать более совершенный алгоритм hashCode, чтобы устранить коллизию.   -  person Ashish Ani    schedule 01.12.2015
comment
Если вы проверите документацию метода генерации String hashcode (), они реализуют формулу, основанную на символах в строке.   -  person Ashish Ani    schedule 01.12.2015
comment
Все зависит от того, что вы на самом деле делаете с хэш-кодом. В конечном итоге это компромисс между усилиями по реализации, затратами времени выполнения и идеальным количеством конфликтов, которые вы можете допустить. Нет «лучше». В случае сомнений используйте один из удобных конструкторов хэш-кода, например, из guava или просто верните -1, если вам действительно все равно. В качестве альтернативы подумайте, возможно, об использовании MD5 или Murmur, чтобы получить приличный разброс значений при не слишком больших накладных расходах на вычисления. Я на собственном горьком опыте убедился, что String.hashCode () и HashMaps не масштабируются так далеко, как несколько десятков тысяч записей.   -  person Jilles van Gurp    schedule 07.12.2015


Ответы (5)


Как это значение поможет уменьшить количество столкновений?

Конфликт хэша в первую очередь достигается за счет хорошего распределения по всему диапазону хеширования (здесь целочисленный тип).

Определив 0 в качестве начального значения для вычисления результата хеширования, вы получите несколько ограниченное распределение в небольшом диапазоне. Объекты, которые отличаются незначительно - возможно, только в каком-то поле - создают хэш-коды, которые находятся недалеко друг от друга. Это повышает вероятность хеш-коллизий.

Определяя ненулевое начальное значение, вы просто увеличиваете промежутки между вычисленными хэш-кодами для объектов, которые отличаются лишь незначительно. Таким образом, вам лучше использовать диапазон хеширования и эффективно снизить вероятность столкновения хешей.

Что он имеет в виду, говоря, что точное значение, возвращаемое hashCode, является функцией значения экземпляра?

Это просто означает, что вы должны рассчитать хэш-код, используя значение объекта, то есть значения его полей. Вы уже сделали это в своем примере, и я думаю, что вы уже неявно это поняли.

Но: Джошуа Блох намеревался сказать кое-что еще в этом абзаце: он хотел предупредить вас о отсутствии документирования точной функции, как вычисляется хэш-код. Если вы это сделаете, вы ограничитесь тем, что больше не сможете изменять реализацию в будущих выпусках, потому что некоторые пользователи могут ожидать определенной реализации, и вы можете сломать некоторый код в зависимости от вашего.

person Seelenvirtuose    schedule 01.12.2015
comment
Когда вы говорите: «Определяя ненулевое начальное значение, вы просто увеличиваете промежутки между вычисленными хэш-кодами для объектов, которые отличаются лишь незначительно ...», есть ли шанс, что вы могли бы отредактировать свой вопрос с помощью простого примера того, когда что бы было так? Я изо всех сил пытаюсь понять, как это работает, спасибо :-) - person Zippy; 16.12.2020

См. Этот пример:

    String a = "Abc";
    String b = "Abc";
    String c = "Pqr";
    System.out.println(" "+a.hashCode()+" "+b.hashCode()+" "+c.hashCode());

Выход: 65602 65602 80497

Это ясно показывает, что hashCode () строки зависит от значений.

Выдержка из документации hashCode ():
int java.lang.String.hashCode ()

Возвращает хэш-код для этой строки. Хэш-код для объекта String вычисляется как

s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]

с использованием арифметики int, где s [i] - i-й символ строки, n - длина строки, а ^ указывает возведение в степень. (Хеш-значение пустой строки равно нулю.)

person Ashish Ani    schedule 01.12.2015
comment
Ваше наблюдение имеет для меня смысл. Я пришел к выводу, что в случае вышеупомянутых классов, если метод hashCode зависит от переменных экземпляра, может быть трудно переписать этот метод хеширования в будущей версии без нарушения кода предыдущей версии. Например, 2 строки, которые логически равны (когда их хэш-коды вычисляются с использованием содержащихся в них символов), могут стать неравными, если будущее определение hashCode принимает во внимание некоторые другие параметры. В некотором смысле существует тесная связь между hashCode и экземпляром, что всегда плохо. - person gaurav jain; 01.12.2015
comment
Но я до сих пор не совсем понимаю первую часть. Можете ли вы указать причину, по которой начальное значение для 'result' не используется как '0'? Как это поможет уменьшить количество столкновений? - person gaurav jain; 01.12.2015

Реализация hashCode в Effective Java специально инструктирует вас выбрать ненулевое значение для начального значения результата. Что касается вашего второго вопроса, hashCode предполагается для получения того же значения, когда внутреннее состояние, используемое для равных сравнений объекта, одинаково. Таким образом, тот факт, что вы получите одно и то же значение, когда все переменные экземпляра равны нулю, выполняет контракт hashCode. Обратите внимание, что весь подзаголовок - «Всегда переопределять hashCode, когда вы переопределяете равно».

person pvg    schedule 01.12.2015

Что касается вашего первого вопроса, если 2 объекта равны, они должны возвращать одно и то же значение хеш-функции, это причина, по которой переопределение метода хеширования является хорошей идеей, когда вы переопределяете метод equals. Это не позволяет избежать столкновений одинаковых объектов, но снижает вероятность столкновений, когда объекты не равны, что более важно, поскольку мы хотим иметь возможность как можно быстрее находить уникальные объекты.

Что касается вашего второго вопроса, я не претендую на большой опыт разработки хеш-кода, однако я считаю, что он имеет в виду, что определенные объекты могут возвращать только одно значение хеша (например, синглтон).

Он говорит, что размещение этого значения в документации - плохая практика, поскольку вы можете захотеть изменить хеш-функцию позже, или другие переменные в хеш-функции могут измениться позже, изменив возвращаемое значение.

В любом случае указывать возвращаемое значение или полагаться на указанное возвращаемое значение - плохая идея.

person yitzih    schedule 01.12.2015
comment
мой первый вопрос касается начального значения результата, отличного от 0? Как это поможет уменьшить количество столкновений? Я знаю, что мне нужно переопределить hashCode, когда я переопределю equals. Это больше о разработке метода хеширования. - person gaurav jain; 01.12.2015
comment
Ах хорошо, я неправильно понял. Не следует использовать не только 0, но и любое непростое число. (Большие простые числа составляют лучшие алгоритмы хеширования.) Это потому, что операции с простыми числами с гораздо меньшей вероятностью приведут к аналогичным результатам. 0 - это просто наихудшее непростое число, потому что результат часто будет одинаковым для всех чисел (т.е. 10 * 0 равно 0, а значит, и 15 * 0). - person yitzih; 01.12.2015

Прежде всего хочу сказать очень важную вещь, которую часто нечетко формулируют:

Внедрение хэш-кода для БОЛЬШИНСТВА СЛУЧАЕВ НЕ ВАЖНО. Это сводится только к проблеме производительности. Поэтому, если у вас есть проблема с хэш-кодом и идентификатором объекта, просто верните -1. У вас будет низкая производительность, но надежная и правильная реализация. Но пока у вас не будут тысячи объектов, использующих хэш-код, вы не заметите низкой производительности. кстати: «Столкновение» выглядит значимым словом в контексте хэш-кода. Да, но только если производительность действительно является проблемой. «Столкновение» значений хэш-кода не означает, что ваша программа работает некорректно. Это означает, что ваша программа может работать медленнее. Поскольку доступ с ключом к карте вызовет последовательную итерацию по объектам с тем же хэш-кодом. В высокопроизводительных средах это может быть проблемой. В большинстве случаев нет.

Но что ВАЖНО, если вы переопределяете хэш-код: вы должны реализовать его ПРАВИЛЬНО. Таким образом, определение всегда должно выполняться: если equals возвращает true, хэш-код должен возвращать то же значение.

Еще одна вещь: хотя вы случайно не столкнетесь с проблемами с этим, вычисление хэш-кода на неизменяемых значениях - плохая идея. Это связано с тем, что после использования хэш-кода объект помещается в специальную позицию на «Карте». При изменении значений, от которых зависит хэш-код, этот объект может быть утерян или станет труднодоступным. Это повлияет на правильность вашей программы.

Вывод: используйте хэш-код только в том случае, если вам действительно нужна производительность. И тогда вы должны убедиться, что наносите его правильно. Здесь легко сделать ошибки, но эти ошибки бывает труднее всего идентифицировать.

person oopexpert    schedule 01.12.2015
comment
Хотя это правда, что вам не следует беспокоиться о создании современной хэш-функции, если она вам не нужна, и что хеш-функции обычно не импортируются, вам никогда не следует просто возвращать какое-то постоянное значение. Если вам нужна хеш-функция, не требующая усилий, просто используйте Objects.hash(field1, field2, field3). - person Kevin; 19.02.2016
comment
Что касается вашего другого предложения об использовании только неизменяемых полей, я считаю, что это ошибочное мнение. Лично я был бы более сбит с толку / обеспокоен, если бы использовал объект в качестве ключа в HashMap, изменил его и все еще мог бы получить значение из HashMap; Я бы удивился, если, например, что-то произошло, и это на самом деле не мутировало. - person Kevin; 19.02.2016
comment
Возвращение -1 - надежное резервное решение, когда ваш хэш-код поврежден, и от него зависит позиция объекта в хэш-дереве. И ваш метод хэш-кода поврежден, если он использует изменяемые поля. Попробуйте сами: поместите объект в хэш-карту (как ключ) или в Hashset и измените поля, которые используются в методе хэш-кода. Объект становится недоступным, потому что теперь он находится в состоянии, которое не соответствует корзине, в которую он был помещен ранее. Кстати, вы не объяснили, почему возвращение -1 никогда не должно возвращаться, поскольку я четко заявил, что это запасной вариант, когда возникают проблемы с ним. - person oopexpert; 19.02.2016
comment
Точно. Каждый объект, с которым вы, вероятно, знакомы, станет недоступным; это ожидаемое поведение. - person Kevin; 19.02.2016
comment
Вы не должны использовать патологическую хеш-функцию, потому что от этого просто нет пользы. В общем, изменение ключей - плохая идея, поэтому снижение производительности контейнеров на основе хешей просто для того, чтобы позволить себе их видоизменить, просто не лучший компромисс. Что касается запасного комментария; выполнить hashCode() контракт несложно. - person Kevin; 19.02.2016