Послідовність hashCode () в рядку Java


134

Значення хеш- коду Java-рядка обчислюється як ( String.hashCode () ):

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

Чи існують обставини (скажімо, версія JVM, постачальник тощо), за яких наступний вираз буде оцінюватися як хибний?

boolean expression = "This is a Java string".hashCode() == 586653468

Оновлення №1: Якщо ви стверджуєте, що відповідь "так, існують такі обставини" - тоді, будь ласка, наведіть конкретний приклад, коли "Це рядок Java" .hashCode ()! = 586653468. Намагайтеся бути максимально конкретними / конкретними якомога.

Оновлення №2: Усі ми знаємо, що покладатися на деталі реалізації hashCode () взагалі погано. Однак я говорю конкретно про String.hashCode () - тому, будь ласка, тримайте відповідь, зосереджену на String.hashCode (). Object.hashCode () абсолютно не має значення в контексті цього питання.


2
Вам справді потрібна ця функціональність? Навіщо потрібне точне значення?
Брайан Агнеу,

26
@Brian: Я намагаюся зрозуміти договір String.hashCode ().
knorv

3
@Knorv Не потрібно розуміти, як саме він працює - важливіше зрозуміти договір та його зовнішній зміст.
мП.

45
@mP: Дякую за ваш внесок, але я думаю, що саме я вирішу.
knorv

чому вони дали першому персонажу найбільшу силу? коли ви хочете оптимізувати її для швидкості, щоб зберегти зайві обчислення, ви зберегли б потужність попереднього, але попередній був би від останнього символу до першого. це означає, що також будуть пропуски кешу. хіба не більш ефективно мати алгоритм: s [0] + s [1] * 31 + s [2] * 31 ^ 2 + ... + s [n-1] * 31 ^ [n-1 ]?
андроїд розробник

Відповіді:


101

Я бачу цю документацію ще в Java 1.2.

Хоча це правда, що в цілому ви не повинні покладатися на те, щоб хеш-код залишався таким самим, тепер це документально підтверджено java.lang.String, тому що зміна може вважатись розривом існуючих контрактів.

Скрізь , де це можливо, ви не повинні покладатися на хеш - кодів залишаються тими ж в різних версіях і т.д. - але в моїй свідомості java.lang.Stringце особливий випадок , просто тому , що алгоритм був заданий ... до тих пір , поки ви готові відмовитися від сумісності з випусками до алгоритм був визначений, звичайно.


7
Документоване поведінка String було визначено з Java 1.2 У версії 1.1 API, обчислення хеш-коду не вказано для класу String.
Мартін Оконнор

У цьому випадку нам краще написати власний хешируючий код «migh matey»?
Феліпе

@Felype: Я дійсно не знаю, що ти тут намагаєшся сказати, боюся.
Джон Скіт

@JonSkeet Я маю на увазі, у цьому випадку ми, можливо, можемо написати власний код для створення власного хешу та надання портативності. Є це?
Феліпе

@Felype: Зовсім не зрозуміло, про яку портативність ви говорите, а також про те, що ви маєте на увазі під "у цьому випадку" - в якому конкретному сценарії? Підозрюю, вам слід задати нове запитання.
Джон Скіт

18

Я знайшов щось про JDK 1.0 та 1.1 та> = 1.2:

У JDK 1.0.x та 1.1.x функція хеш-коду для довгих рядків працювала шляхом вибірки кожного n-го символу. Це досить добре гарантовано, що у вас буде багато струнів хешування на одне значення, тим самим уповільнюючи пошук Hashtable У JDK 1.2 функція була вдосконалена для множення результату досі на 31, а потім додавання наступного символу послідовно. Це трохи повільніше, але набагато краще уникнути зіткнень. Джерело: http://mindprod.com/jgloss/hashcode.html

Щось інше, тому що вам, здається, потрібне число: Як щодо використання CRC32 або MD5 замість хеш-коду, і вам добре піти - ніяких дискусій і жодних турбот ...


8

Не слід покладатися на те, що хеш-код дорівнює певному значенню. Просто те, що воно поверне стійкі результати в межах одного виконання. Документи API кажуть наступне:

Загальним контрактом hashCode є:

  • Кожного разу, коли він викликається на одному і тому ж об'єкті не раз під час виконання програми Java, метод hashCode повинен послідовно повертати одне ціле ціле число, за умови, що жодна інформація, що використовується в рівних порівняннях на об'єкті, не змінюється. Це ціле число не повинно залишатися послідовним від одного виконання програми до іншого виконання тієї самої програми.

EDIT Оскільки javadoc для String.hashCode () визначає, як обчислюється хеш-код String, будь-яке порушення цього може порушити специфікацію публічного API.


1
Ваша відповідь дійсна, але не стосується конкретного поставленого питання.
knorv

6
Це загальний контракт хеш-коду - але конкретний контракт для String дає детальну інформацію про алгоритм і фактично перекриває цей загальний контракт IMO.
Джон Скіт

4

Як було сказано вище, загалом не слід покладатися на хеш-код класу, який залишається тим самим. Зауважте, що навіть наступні запуски одного і того ж додатка на одній VM можуть створювати різні значення хешу. AFAIK хеш-функція Sun JVM обчислює один і той же хеш на кожному запуску, але це не гарантується.

Зауважте, що це не теоретично. Хеш-функція для java.lang.String була змінена в JDK1.2 (старий хеш мав проблеми з ієрархічними рядками, такими як URL-адреси або імена файлів, оскільки він, як правило, створював той самий хеш для рядків, який відрізнявся лише в кінці).

java.lang.String - особливий випадок, оскільки алгоритм його hashCode () документально (зараз) задокументований, тому ви, напевно, можете покластися на це. Я б все-таки вважав це поганою практикою. Якщо вам потрібен алгоритм хеш із спеціальними, задокументованими властивостями, просто напишіть один :-).


4
Але чи був алгоритм вказаний у документах до JDK 1.2? Якщо ні, то інша ситуація. Тепер алгоритм закладений у документах, тож змінити його було б суттєвою зміною державного контракту.
Джон Скіт

(Я пам'ятаю це як 1.1.) Оригінальний (бідніший) алгоритм був задокументований. Неправильно. Задокументований алгоритм фактично викинув ArrayIndexOutOfBoundsException.
Том Хотін - тайклін

@Jon Skeet: Ах, не знав, що алгоритм String.hashCode () задокументований. Звичайно, що змінює речі. Оновлено мій коментар.
sleske

3

Ще одне (!) Питання, про яке слід турбувати, - це можлива зміна впровадження між ранньою та пізньою версіями Java. Я не вірю, що відомості про впровадження встановлені в камені, і тому потенційно оновлення до майбутньої версії Java може спричинити проблеми.

Суть полягає в тому, що я б не покладався на реалізацію hashCode().

Можливо, ви можете виділити, яку проблему ви насправді намагаєтеся вирішити, використовуючи цей механізм, і це підкреслить більш підходящий підхід.


1
Дякую за вашу відповідь. Чи можете ви навести конкретні приклади, коли "Це рядок Java" .hashCode ()! = 586653468?
knorv

1
Ні вибач. Моя думка, що все, на що ви тестуєтесь, може працювати так, як вам хочеться. Але це все ще не гарантує. Отже, якщо ви працюєте над (скажімо) короткостроковим проектом, де ви маєте контроль над машиною управління тощо, то вищезгадане може працювати для вас. Але ви не можете розраховувати на це в широкому світі.
Брайан Агнеу

2
"оновлення до майбутньої версії Java може спричинити проблеми". Оновлення до майбутньої версії Java може повністю видалити метод hashCode. Або змушуйте завжди повертати 0 для рядків. Це несумісні зміни для вас. Питання полягає в тому, чи вважає Sun ^ HOracle ^ HThe JCP це переломною зміною і тому варто уникати. Оскільки алгоритм є у договорі, сподіваємось, що вони будуть.
Стів Джессоп

@SteveJessop добре, оскільки switchзаяви над рядками компілюються в код, спираючись на певний фіксований хеш-код, зміни в Stringалгоритмі хеш-коду безумовно порушать існуючий код…
Holger

3

Просто відповісти на ваше запитання і не продовжувати ніяких дискусій. Реалізація JDK Apache Harmony, схоже, використовує інший алгоритм, принаймні, це виглядає зовсім інакше:

ВС JDK

public int hashCode() {
    int h = hash;
    if (h == 0) {
        int off = offset;
        char val[] = value;
        int len = count;

        for (int i = 0; i < len; i++) {
            h = 31*h + val[off++];
        }
        hash = h;
    }
    return h;
}

Гармонія Apache

public int hashCode() {
    if (hashCode == 0) {
        int hash = 0, multiplier = 1;
        for (int i = offset + count - 1; i >= offset; i--) {
            hash += value[i] * multiplier;
            int shifted = multiplier << 5;
            multiplier = shifted - multiplier;
        }
        hashCode = hash;
    }
    return hashCode;
}

Сміливо переконайтеся самі ...


23
Я думаю, що вони просто круті і оптимізують це. :) "(множник << 5) - множник" - це всього лише 31 * множник, зрештою ...
розмотайте

Гаразд, було лінь це перевірити. Дякую!
ReneS

1
Але щоб було зрозуміло з мого боку ... Ніколи не покладайтеся на хеш-код, тому що хеш-код - це щось внутрішнє.
ReneS

1
що означають змінні "offset", "count" та "hashCode"? я припускаю, що "хеш-код" використовується як кешоване значення, щоб уникнути майбутніх обчислень, і що "підрахунок" - це кількість символів, але що таке "зміщення"? припустимо, я хочу використовувати цей код, щоб він був послідовним, задавши рядок, що мені робити з ним?
андроїд розробник

1
@androiddeveloper Тепер це цікаве запитання - хоча я мусив це здогадатися, виходячи з вашого імені користувача. З документів Android виглядає, що контракт такий же: s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]якщо я не помиляюся, це тому, що Android використовує Sun реалізацію об'єкта String без змін.
Картік Ч'ю

2

Якщо ви турбуєтесь про зміни та, можливо, несумісні VM, просто скопіюйте існуючу реалізацію хеш-коду у свій власний клас утиліти та використовуйте її для створення своїх хеш-кодів.


Я збирався це сказати. Хоча інші відповіді відповідають на питання, написання окремої функції хеш-коду, ймовірно, є відповідним рішенням проблеми knorv.
Нік

1

Хеш-код буде розрахований на основі значень ASCII символів у рядку.

Це реалізація в String Class наступним чином

public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        hash = h = isLatin1() ? StringLatin1.hashCode(value)
                              : StringUTF16.hashCode(value);
    }
    return h;
}

Зіткнення у хеш-коді неминучі. Наприклад, рядки "Ea" і "FB" дають той же хеш-код, що і 2236

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.