Spójność hashCode () w ciągu Java

138

Wartość hashCode ciągu Java jest obliczana jako ( String.hashCode () ):

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

Czy są jakieś okoliczności (np. Wersja maszyny JVM, dostawca itp.), W których poniższe wyrażenie zostanie uznane za fałszywe?

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

Aktualizacja nr 1: Jeśli twierdzisz, że odpowiedź brzmi „tak, istnieją takie okoliczności” - podaj konkretny przykład, kiedy „To jest ciąg języka Java” .hashCode ()! = 586653468. Spróbuj być tak konkretny / konkretny jak to możliwe.

Aktualizacja # 2: Wszyscy wiemy, że poleganie na szczegółach implementacji funkcji hashCode () jest ogólnie złe. Jednak mówię konkretnie o String.hashCode () - więc proszę, skup się na odpowiedzi na String.hashCode (). Object.hashCode () jest całkowicie nieistotna w kontekście tego pytania.

knorv
źródło
2
Czy faktycznie potrzebujesz tej funkcjonalności? Dlaczego potrzebujesz dokładnej wartości?
Brian Agnew
26
@Brian: Próbuję zrozumieć kontrakt String.hashCode ().
knorv
3
@Knorv Nie trzeba dokładnie rozumieć, jak to działa - ważniejsze jest zrozumienie umowy i jej ukrytego znaczenia.
mP.
46
@mP: Dziękuję za wkład, ale myślę, że decyzja należy do mnie.
knorv
dlaczego dali pierwszemu bohaterowi największą moc? jeśli chcesz zoptymalizować go pod kątem szybkości, aby zachować dodatkowe obliczenia, przechowujesz moc poprzedniego, ale poprzednia byłaby od ostatniego znaku do pierwszego. oznacza to, że wystąpiłyby również błędy w pamięci podręcznej. czy nie jest bardziej wydajne mieć algorytm: s [0] + s [1] * 31 + s [2] * 31 ^ 2 + ... + s [n-1] * 31 ^ [n-1 ]?
programista Androida

Odpowiedzi:

103

Widzę tę dokumentację od czasów Java 1.2.

Chociaż prawdą jest, że generalnie nie powinieneś polegać na tym, że implementacja kodu skrótu pozostanie taka sama, jest to teraz udokumentowane zachowanie java.lang.String, więc zmiana będzie liczyła się jako zerwanie istniejących kontraktów.

O ile to możliwe, nie należy polegać na kody hash pobytu takie same w całej wersji itp - ale w moim umyśle java.lang.Stringjest szczególnym przypadkiem po prostu dlatego, że algorytm nie został określony ... tak długo, jak jesteś gotów porzucić zgodność z wersjami przed złożeniem oczywiście określono algorytm.

Jon Skeet
źródło
7
Udokumentowane zachowanie String zostało określone od wersji Java 1.2. W wersji 1.1 interfejsu API obliczenia skrótu nie są określone dla klasy String.
Martin OConnor
W takim razie lepiej napiszmy nasze własne haszujące kody.
Felype
@Felype: Naprawdę nie wiem, co próbujesz tutaj powiedzieć, obawiam się.
Jon Skeet
@JonSkeet Mam na myśli to, że w tym przypadku możemy napisać własny kod, aby wygenerować własny hash, aby zapewnić przenośność. Czy to jest?
Felype,
@Felype: Nie jest wcale jasne, o jakim rodzaju przenośności mówisz, ani też co masz na myśli, mówiąc „w tym przypadku” - w jakim konkretnym scenariuszu? Podejrzewam, że powinieneś zadać nowe pytanie.
Jon Skeet,
18

Znalazłem coś o JDK 1.0 i 1.1 i> = 1.2:

W JDK 1.0.x i 1.1.x funkcja hashCode dla długich ciągów działała na zasadzie próbkowania każdego n-tego znaku. To całkiem dobrze gwarantuje, że będziesz miał wiele ciągów haszujących do tej samej wartości, co spowolni wyszukiwanie Hashtable. W JDK 1.2 ulepszono funkcję polegającą na pomnożeniu wyniku przez 31, a następnie dodaniu kolejnego znaku w kolejności. Jest to trochę wolniejsze, ale znacznie lepsze w unikaniu kolizji. Źródło: http://mindprod.com/jgloss/hashcode.html

Coś innego, ponieważ wydaje się, że potrzebujesz numeru: Co powiesz na używanie CRC32 lub MD5 zamiast kodu skrótu i ​​jesteś gotowy - żadnych dyskusji i żadnych zmartwień ...

ReneS
źródło
8

Nie należy polegać na tym, że kod skrótu jest równy określonej wartości. Po prostu zwróci spójne wyniki w ramach tego samego wykonania. Dokumentacja API zawiera następujące informacje:

Ogólna umowa dotycząca hashCode to:

  • Za każdym razem, gdy jest wywoływana na tym samym obiekcie więcej niż jeden raz podczas wykonywania aplikacji Java, metoda hashCode musi konsekwentnie zwracać tę samą liczbę całkowitą, pod warunkiem, że żadne informacje użyte w porównaniach równości na obiekcie nie zostaną zmodyfikowane. Ta liczba całkowita nie musi pozostawać spójna od jednego wykonania aplikacji do innego wykonania tej samej aplikacji.

EDYCJA Ponieważ javadoc dla String.hashCode () określa, w jaki sposób obliczany jest kod skrótu String, każde naruszenie tego stanowiłoby naruszenie specyfikacji publicznego API.

Martin OConnor
źródło
1
Twoja odpowiedź jest prawidłowa, ale nie dotyczy konkretnego zadanego pytania.
knorv
6
To jest ogólna umowa na kod skrótu - ale konkretna umowa dotycząca String zawiera szczegóły algorytmu i skutecznie zastępuje tę umowę generalną IMO.
Jon Skeet
4

Jak wspomniano powyżej, generalnie nie należy polegać na kodzie skrótu klasy, który pozostaje taki sam. Należy pamiętać, że nawet kolejne uruchomienia tej samej aplikacji na tej samej maszynie wirtualnej mogą generować różne wartości skrótu. AFAIK funkcja skrótu Sun JVM oblicza ten sam skrót przy każdym uruchomieniu, ale nie jest to gwarantowane.

Zauważ, że nie jest to teoretyczne. Funkcja skrótu dla java.lang.String została zmieniona w JDK1.2 (stary hash miał problemy z ciągami hierarchicznymi, takimi jak adresy URL lub nazwy plików, ponieważ miał tendencję do tworzenia tego samego skrótu dla łańcuchów, które różniły się tylko na końcu).

java.lang.String to szczególny przypadek, ponieważ algorytm jego hashCode () jest (teraz) udokumentowany, więc prawdopodobnie możesz na tym polegać. Nadal uważam to za złą praktykę. Jeśli potrzebujesz algorytmu haszującego ze specjalnymi, udokumentowanymi właściwościami, napisz go :-).

sleske
źródło
4
Ale czy algorytm został określony w dokumentacji przed JDK 1.2? Jeśli nie, to inna sytuacja. Algorytm jest teraz przedstawiony w dokumentacji, więc zmiana byłaby przełomową zmianą w zamówieniu publicznym.
Jon Skeet
(Pamiętam to jako 1.1.) Oryginalny (gorszy) algorytm został udokumentowany. Nieprawidłowo. Udokumentowany algorytm faktycznie wyrzucił wyjątek ArrayIndexOutOfBoundsException.
Tom Hawtin - tackline
@Jon Skeet: Ach, nie wiedziałem, że algorytm String.hashCode () jest udokumentowany. Oczywiście to wszystko zmienia. Zaktualizowałem mój komentarz.
sleske
3

Kolejną (!) Kwestią, o którą należy się martwić, jest możliwa zmiana implementacji między wczesnymi a późnymi wersjami Javy. Nie wierzę, że szczegóły implementacji są nieodwracalne, więc potencjalnie aktualizacja do przyszłej wersji Java może spowodować problemy.

Podsumowując, nie polegałbym na implementacji hashCode().

Być może możesz wskazać problem, który faktycznie próbujesz rozwiązać za pomocą tego mechanizmu, a to podkreśli bardziej odpowiednie podejście.

Brian Agnew
źródło
1
Dzięki za odpowiedź. Czy możesz podać konkretne przykłady, kiedy „To jest ciąg Java” .hashCode ()! = 586653468?
knorv
1
Nie. Przepraszam. Chodzi mi o to, że wszystko, na czym testujesz, może działać tak, jak chcesz. Ale to wciąż nie gwarantuje. Więc jeśli pracujesz nad (powiedzmy) krótkoterminowym projektem, w którym masz kontrolę nad maszyną wirtualną itp., To powyższe może działać dla Ciebie. Ale nie można na tym polegać w szerszym świecie.
Brian Agnew
2
„aktualizacja do przyszłej wersji Java może powodować problemy”. Aktualizacja do przyszłej wersji Java może całkowicie usunąć metodę hashCode. Lub spraw, aby zawsze zwracał 0 dla łańcuchów. To dla ciebie niekompatybilne zmiany. Pytanie brzmi, czy Sun ^ HOracle ^ HJCP uznałby to za przełomową zmianę i dlatego warto jej unikać. Ponieważ algorytm jest w kontrakcie, można mieć nadzieję, że tak.
Steve Jessop
@SteveJessop no cóż, ponieważ switchinstrukcje na łańcuchach kompilują się do kodu w oparciu o konkretny stały kod skrótu, zmiany w Stringalgorytmie kodu skrótu z pewnością złamałyby istniejący kod…
Holger
3

Wystarczy odpowiedzieć na Twoje pytanie i nie kontynuować dyskusji. Wydaje się, że implementacja Apache Harmony JDK używa innego algorytmu, przynajmniej wygląda zupełnie inaczej:

Sun 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 Harmony

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;
}

Zapraszam do samodzielnego sprawdzenia ...

ReneS
źródło
23
Myślę, że po prostu są fajni i optymalizują to. :) "(mnożnik << 5) - mnożnik" to tylko 31 * mnożnik, w końcu ...
rozwiń
Ok, był zbyt leniwy, żeby to sprawdzić. Dzięki!
RENES
1
Ale żeby było jasne z mojej strony ... Nigdy nie polegaj na hashcode, ponieważ hashcode jest czymś wewnętrznym.
RENES
1
co oznaczają zmienne „offset”, „count” i „hashCode”? przypuszczam, że „hashcode” jest używany jako wartość w pamięci podręcznej, aby uniknąć przyszłych obliczeń, a „count” to liczba znaków, ale co to jest „offset”? przypuśćmy, że chcę użyć tego kodu, aby był spójny, biorąc pod uwagę ciąg, co mam z tym zrobić?
programista Androida
1
@androiddeveloper Teraz TO interesujące pytanie - chociaż powinienem się domyślić na podstawie Twojej nazwy użytkownika. Z dokumentacji Androida wygląda na to, że umowa jest taka sama: s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]O ile się nie mylę, dzieje się tak, ponieważ Android używa implementacji obiektu String firmy Sun bez żadnych zmian.
Kartik Chugh,
2

Jeśli martwisz się zmianami i prawdopodobnie niekompatybilnymi maszynami wirtualnymi, po prostu skopiuj istniejącą implementację kodu skrótu do własnej klasy narzędziowej i użyj jej do wygenerowania kodów skrótów.

Sam Barnum
źródło
Miałem to powiedzieć. Podczas gdy inne odpowiedzi odpowiadają na to pytanie, napisanie oddzielnej funkcji hashCode jest prawdopodobnie odpowiednim rozwiązaniem problemu knorv.
Nick,
1

Kod skrótu zostanie obliczony na podstawie wartości ASCII znaków w łańcuchu.

Oto implementacja w klasie String

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

Kolizje w hashcode są nieuniknione. Na przykład ciągi „Ea” i „FB” dają ten sam kod skrótu co 2236

Lourdes
źródło