Coerenza di hashCode () su una stringa Java


134

Il valore hashCode di una stringa Java viene calcolato come ( String.hashCode () ):

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

Vi sono circostanze (ad esempio versione JVM, fornitore, ecc.) In base alle quali la seguente espressione verrà valutata come falsa?

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

Aggiornamento n. 1: se affermi che la risposta è "sì, ci sono circostanze del genere" - per favore fai un esempio concreto di quando "Questa è una stringa Java" .hashCode ()! = 586653468. Prova ad essere specifico / concreto il più possibile.

Aggiornamento n. 2: Sappiamo tutti che fare affidamento sui dettagli di implementazione di hashCode () è generalmente negativo. Tuttavia, sto parlando specificamente di String.hashCode () - quindi ti preghiamo di mantenere la risposta focalizzata su String.hashCode (). Object.hashCode () è totalmente irrilevante nel contesto di questa domanda.


2
Hai davvero bisogno di questa funzionalità? Perché hai bisogno del valore preciso?
Brian Agnew,

26
@Brian: sto cercando di capire il contratto di String.hashCode ().
Knorv,

3
@Knorv Non è necessario capire esattamente come funziona - è più importante capire il contratto e il suo ulteriore significato.
mP.

45
@mP: grazie per il tuo contributo, ma credo che spetta a me decidere.
Knorv,

perché hanno dato al primo personaggio il potere più grande? quando si desidera ottimizzarlo per la velocità al fine di preservare i calcoli extra, si memorizza la potenza del precedente, ma il precedente sarebbe dall'ultimo carattere al primo. questo significa che ci sarebbero anche mancati cache. non è più efficiente avere un algoritmo di: s [0] + s [1] * 31 + s [2] * 31 ^ 2 + ... + s [n-1] * 31 ^ [n-1 ]?
sviluppatore Android il

Risposte:


101

Vedo quella documentazione fin da Java 1.2.

Mentre è vero che in generale non dovresti fare affidamento sul fatto che un'implementazione del codice hash rimanga la stessa, ora è un comportamento documentato java.lang.String, quindi cambiarlo conterebbe come rompere i contratti esistenti.

Ove possibile, non dovresti fare affidamento sul fatto che i codici hash rimangano gli stessi tra le versioni ecc., Ma nella mia mente java.lang.Stringè un caso speciale semplicemente perché l'algoritmo è stato specificato ... purché tu sia disposto ad abbandonare la compatibilità con le versioni prima del l'algoritmo è stato specificato, ovviamente.


7
Il comportamento documentato di String è stato specificato da Java 1.2 Nella v1.1 dell'API, il calcolo del codice hash non è specificato per la classe String.
Martin OConnor,

In questo caso è meglio che scriviamo i nostri codici di hashing ight matey?
Felype,

@Felype: davvero non so cosa stai cercando di dire qui, temo.
Jon Skeet,

@JonSkeet Voglio dire, in questo caso potremmo forse scrivere il nostro codice per generare il nostro hash, per garantire la portabilità. È?
Felype,

@Felype: Non è affatto chiaro di che tipo di portabilità stai parlando, né in effetti cosa intendi per "in questo caso" - in quale specifico scenario? Sospetto che dovresti fare una nuova domanda.
Jon Skeet,

18

Ho trovato qualcosa su JDK 1.0 e 1.1 e> = 1.2:

In JDK 1.0.xe 1.1.x la funzione hashCode per stringhe lunghe ha funzionato campionando ogni ennesimo carattere. Ciò garantisce che molte stringhe abbiano lo stesso valore, rallentando così la ricerca Hashtable. In JDK 1.2 la funzione è stata migliorata per moltiplicare il risultato finora per 31, quindi aggiungere il carattere successivo in sequenza. Questo è un po 'più lento, ma è molto meglio per evitare le collisioni. Fonte: http://mindprod.com/jgloss/hashcode.html

Qualcosa di diverso, perché sembra che tu abbia bisogno di un numero: che ne dici di usare CRC32 o MD5 invece di hashcode e sei a posto - nessuna discussione e nessuna preoccupazione ...


8

Non devi fare affidamento sul fatto che un codice hash sia uguale a un valore specifico. Solo che restituirà risultati coerenti all'interno della stessa esecuzione. I documenti API dicono quanto segue:

Il contratto generale di hashCode è:

  • Ogni volta che viene invocato sullo stesso oggetto più di una volta durante un'esecuzione di un'applicazione Java, il metodo hashCode deve restituire costantemente lo stesso numero intero, a condizione che non vengano modificate le informazioni utilizzate nei confronti uguali sull'oggetto. Questo numero intero non deve rimanere coerente da un'esecuzione di un'applicazione a un'altra esecuzione della stessa applicazione.

EDIT Poiché javadoc per String.hashCode () specifica come viene calcolato il codice hash di una stringa, qualsiasi violazione di ciò violerebbe la specifica dell'API pubblica.


1
La tua risposta è valida, ma non risponde alla domanda specifica posta.
Knorv,

6
Questo è il contratto di codice hash generale , ma il contratto specifico per String fornisce i dettagli dell'algoritmo e sostituisce efficacemente questo contratto generale IMO.
Jon Skeet,

4

Come detto sopra, in generale non dovresti fare affidamento sul fatto che il codice hash di una classe rimanga lo stesso. Si noti che anche le esecuzioni successive della stessa applicazione sulla stessa macchina virtuale possono produrre valori hash diversi. AFAIK la funzione hash di Sun JVM calcola lo stesso hash ad ogni corsa, ma ciò non è garantito.

Si noti che questo non è teorico. La funzione hash per java.lang.String è stata modificata in JDK1.2 (il vecchio hash aveva problemi con stringhe gerarchiche come URL o nomi di file, poiché tendeva a produrre lo stesso hash per stringhe che differivano solo alla fine).

java.lang.String è un caso speciale, poiché l'algoritmo del suo hashCode () è (ora) documentato, quindi probabilmente puoi fare affidamento su questo. Lo considererei ancora una cattiva pratica. Se hai bisogno di un algoritmo hash con proprietà speciali e documentate, basta scriverne uno :-).


4
Ma l'algoritmo era specificato nei documenti prima di JDK 1.2? Altrimenti, è una situazione diversa. L'algoritmo è ora definito nei documenti, quindi cambiarlo sarebbe una rottura del contratto pubblico.
Jon Skeet,

(Lo ricordo come 1.1.) L'algoritmo originale (più scadente) è stato documentato. In modo non corretto. L'algoritmo documentato ha effettivamente generato un'arrayIndexOutOfBoundsException.
Tom Hawtin - tackline

@Jon Skeet: Ah, non sapevo che l'algoritmo di String.hashCode () è documentato. Certo che cambia le cose. Aggiornato il mio commento.
sleske,

3

Un altro (!) Problema di cui preoccuparsi è il possibile cambio di implementazione tra le versioni precedenti / successive di Java. Non credo che i dettagli dell'implementazione siano impostati in pietra, e quindi potenzialmente un aggiornamento a una versione futura di Java potrebbe causare problemi.

In conclusione, non farei affidamento sull'implementazione di hashCode().

Forse puoi evidenziare quale problema stai effettivamente cercando di risolvere usando questo meccanismo, e questo metterà in evidenza un approccio più adatto.


1
Grazie per la tua risposta. Puoi fornire esempi concreti di quando "Questa è una stringa Java" .hashCode ()! = 586653468?
Knorv,

1
No scusa. Il punto è che tutto ciò su cui testate può funzionare nel modo desiderato. Ma questa non è ancora una garanzia. Quindi se stai lavorando a un progetto (diciamo) a breve termine in cui hai il controllo della VM ecc., Allora quanto sopra potrebbe funzionare per te. Ma non puoi fare affidamento su di esso nel resto del mondo.
Brian Agnew,

2
"un aggiornamento a una versione futura di Java potrebbe causare problemi". Un aggiornamento a una versione futura di Java potrebbe rimuovere completamente il metodo hashCode. Oppure fallo sempre restituire 0 per le stringhe. Sono modifiche incompatibili per te. La domanda è se Sun ^ Oracolo ^ HT il JCP lo considererebbe un cambiamento decisivo e quindi vale la pena evitarlo. Dal momento che l'algoritmo è nel contratto, si spera che lo facciano.
Steve Jessop,

@SteveJessop bene, poiché le switchistruzioni sulle stringhe si compilano in base al codice che si basa su un particolare codice hash fisso, le modifiche Stringall'algoritmo del codice hash romperebbero sicuramente il codice esistente ...
Holger

3

Solo per rispondere alla tua domanda e non continuare nessuna discussione. L'implementazione di Apache Harmony JDK sembra utilizzare un algoritmo diverso, almeno sembra totalmente diverso:

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

Sentiti libero di controllarlo tu stesso ...


23
Penso che siano semplicemente fantastici e lo stiano ottimizzando. :) "(moltiplicatore << 5) - moltiplicatore" è solo 31 * moltiplicatore, dopo tutto ...
svolgersi il

Ok, ero troppo pigro per controllarlo. Grazie!
ReneS,

1
Ma per chiarire da parte mia ... Non fare mai affidamento sull'hashcode perché l'hashcode è qualcosa di interno.
ReneS,

1
quali sono le variabili di "offset", "count" e "hashCode"? suppongo che "hashcode" sia usato come valore memorizzato nella cache, per evitare calcoli futuri e che "count" sia il numero di caratteri, ma qual è "offset"? supponiamo che io voglia usare questo codice in modo che sia coerente, data una stringa, cosa dovrei farci?
Sviluppatore Android

1
@androiddeveloper Ora QUESTA È una domanda interessante, anche se avrei dovuto indovinarlo, in base al tuo nome utente. Dai documenti Android sembra che il contratto sia lo stesso: a s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]meno che non mi sbagli, questo è perché Android utilizza l'implementazione Sun dell'oggetto String senza modifiche.
Kartik Chugh,

2

Se sei preoccupato per le modifiche e forse per le VM incompatibili, copia l'implementazione dell'hashcode esistente nella tua classe di utilità e usala per generare i tuoi hashcode.


Stavo per dirlo. Mentre le altre risposte rispondono alla domanda, scrivere una funzione hashCode separata è probabilmente la soluzione appropriata al problema di knorv.
Nick,

1

L'hashcode verrà calcolato in base ai valori ASCII dei caratteri nella stringa.

Questa è l'implementazione nella classe String è la seguente

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

Le collisioni nell'hashcode sono inevitabili. Ad esempio, le stringhe "Ea" e "FB" forniscono lo stesso codice hash di 2236

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.