Konsistensi kode hash () pada string Java

134

Nilai hashCode dari Java String dikomputasi sebagai ( String.hashCode () ):

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

Apakah ada keadaan (katakanlah versi JVM, vendor, dll.) Di mana ungkapan berikut akan dinilai salah?

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

Pembaruan # 1: Jika Anda mengklaim bahwa jawabannya adalah "ya, ada beberapa keadaan" - maka tolong berikan contoh konkret kapan "Ini adalah string Java" .hashCode ()! = 586653468. Cobalah untuk menjadi spesifik / konkret mungkin.

Pembaruan # 2: Kita semua tahu bahwa mengandalkan detail implementasi hashCode () secara umum buruk. Namun, saya sedang berbicara secara khusus tentang String.hashCode () - jadi harap tetap fokus pada String.hashCode (). Object.hashCode () sama sekali tidak relevan dalam konteks pertanyaan ini.

Knorv
sumber
2
Apakah Anda benar-benar membutuhkan fungsi ini? Mengapa Anda membutuhkan nilai yang tepat?
Brian Agnew
26
@Brian: Saya mencoba memahami kontrak dari String.hashCode ().
knorv
3
@Knorv Ini tidak perlu untuk memahami persis cara kerjanya - lebih penting untuk memahami kontrak dan makna tersembunyi.
mP.
45
@ MP: Terima kasih atas masukan Anda, tapi saya rasa itu terserah saya untuk memutuskan.
knorv
mengapa mereka memberi karakter pertama kekuatan terbesar? ketika Anda ingin mengoptimalkannya untuk kecepatan untuk menjaga perhitungan ekstra, Anda akan menyimpan kekuatan yang sebelumnya, namun yang sebelumnya akan dari karakter terakhir ke yang pertama. ini berarti akan ada juga cache miss. bukankah lebih efisien untuk memiliki algoritma: s [0] + s [1] * 31 + s [2] * 31 ^ 2 + ... + s [n-1] * 31 ^ [n-1 ]
pengembang android

Jawaban:

101

Saya bisa melihat dokumentasi itu sejauh Jawa 1.2.

Meskipun memang benar bahwa secara umum Anda tidak harus bergantung pada implementasi kode hash yang tetap sama, namun sekarang perilaku tersebut didokumentasikan java.lang.String, sehingga mengubahnya akan dianggap melanggar kontrak yang ada.

Sedapat mungkin, Anda tidak harus bergantung pada kode hash yang tetap sama di semua versi dll - tetapi dalam pikiran saya java.lang.Stringadalah kasus khusus hanya karena algoritme telah ditentukan ... selama Anda bersedia untuk meninggalkan kompatibilitas dengan rilis sebelum Algoritma ditentukan, tentu saja.

Jon Skeet
sumber
7
Perilaku String yang didokumentasikan telah ditentukan sejak Java 1.2 Dalam v1.1 API, perhitungan kode hash tidak ditentukan untuk kelas String.
Martin OConnor
Dalam hal ini kita lebih baik menulis kode hashing kita sendiri 'matey?
Felype
@Felype: Saya benar-benar tidak tahu apa yang ingin Anda katakan di sini, saya khawatir.
Jon Skeet
@ JonSkeet Maksudku, dalam hal ini kita mungkin dapat menulis kode kita sendiri untuk menghasilkan hash kita sendiri, untuk memberikan portabilitas. Apakah itu?
Felype
@Felype: Sama sekali tidak jelas portabilitas seperti apa yang Anda bicarakan, juga bukan apa yang Anda maksud dengan "dalam kasus ini" - dalam skenario spesifik apa? Saya curiga Anda harus mengajukan pertanyaan baru.
Jon Skeet
18

Saya menemukan sesuatu tentang JDK 1.0 dan 1.1 dan> = 1.2:

Dalam JDK 1.0.x dan 1.1.x fungsi kode hash untuk string panjang bekerja dengan mengambil sampel setiap karakter ke-n. Ini dijamin cukup baik Anda akan memiliki banyak hashing String dengan nilai yang sama, sehingga memperlambat pencarian Hashtable. Dalam JDK 1.2 fungsi telah ditingkatkan untuk mengalikan hasilnya sejauh ini dengan 31 kemudian menambahkan karakter berikutnya secara berurutan. Ini sedikit lebih lambat, tetapi jauh lebih baik dalam menghindari tabrakan. Sumber: http://mindprod.com/jgloss/hashcode.html

Sesuatu yang berbeda, karena Anda tampaknya memerlukan nomor: Bagaimana kalau menggunakan CRC32 atau MD5 alih-alih kode hash dan Anda bisa melakukannya - tidak ada diskusi dan tidak ada kekhawatiran sama sekali ...

Rene
sumber
8

Anda tidak boleh mengandalkan kode hash yang sama dengan nilai tertentu. Hanya saja itu akan mengembalikan hasil yang konsisten dalam eksekusi yang sama. Dokumen API mengatakan sebagai berikut:

Kontrak umum kode hash adalah:

  • Setiap kali ia dipanggil pada objek yang sama lebih dari sekali selama eksekusi aplikasi Java, metode kode hash harus secara konsisten mengembalikan integer yang sama, asalkan tidak ada informasi yang digunakan dalam perbandingan yang setara dengan objek yang dimodifikasi. Bilangan bulat ini tidak harus tetap konsisten dari satu eksekusi aplikasi ke eksekusi aplikasi yang sama.

EDIT Karena javadoc untuk String.hashCode () menentukan bagaimana kode hash String dihitung, setiap pelanggaran ini akan melanggar spesifikasi API publik.

Martin OConnor
sumber
1
Jawaban Anda valid, tetapi tidak menjawab pertanyaan spesifik yang diajukan.
knorv
6
Itulah kontrak kode hash umum - tetapi kontrak spesifik untuk String memberikan detail algoritme, dan secara efektif menimpa IMO kontrak umum ini.
Jon Skeet
4

Seperti dikatakan di atas, secara umum Anda tidak harus bergantung pada kode hash dari kelas yang tetap sama. Perhatikan bahwa bahkan menjalankan aplikasi yang sama pada VM yang sama dapat menghasilkan nilai hash yang berbeda. AFAIK fungsi hash Sun JVM menghitung hash yang sama pada setiap proses, tapi itu tidak dijamin.

Perhatikan bahwa ini bukan teori. Fungsi hash untuk java.lang.String diubah di JDK1.2 (hash lama memiliki masalah dengan string hirarkis seperti URL atau nama file, karena cenderung menghasilkan hash yang sama untuk string yang hanya berbeda di akhir).

java.lang.String adalah kasus khusus, karena algoritme dari kode hash () didokumentasikan, jadi Anda mungkin bisa mengandalkan itu. Saya masih menganggapnya sebagai praktik buruk. Jika Anda memerlukan algoritma hash dengan properti khusus dan terdokumentasi, cukup tulis satu :-).

sleske
sumber
4
Tetapi apakah algoritma yang ditentukan dalam dokumen sebelum JDK 1.2? Jika tidak, ini situasi yang berbeda. Algoritme sekarang ditetapkan dalam dokumen, jadi mengubah itu akan menjadi perubahan melanggar kontrak publik.
Jon Skeet
(Saya ingat sebagai 1.1.) Algoritma asli (lebih buruk) didokumentasikan. Salah. Algoritma yang didokumentasikan sebenarnya melemparkan ArrayIndexOutOfBoundsException.
Tom Hawtin - tackline
@ Jon Skeet: Ah, tidak tahu bahwa algoritma dari String.hashCode () didokumentasikan. Tentu saja itu mengubah banyak hal. Diperbarui komentar saya.
sleske
3

Masalah lain (!) Yang perlu dikhawatirkan adalah kemungkinan perubahan implementasi antara versi Java awal / akhir. Saya tidak percaya detail implementasi diatur dalam batu, dan berpotensi upgrade ke versi Java di masa depan dapat menyebabkan masalah.

Intinya adalah, saya tidak akan bergantung pada implementasi hashCode().

Mungkin Anda bisa menyoroti masalah apa yang sebenarnya ingin Anda selesaikan dengan menggunakan mekanisme ini, dan itu akan menyoroti pendekatan yang lebih cocok.

Brian Agnew
sumber
1
Terima kasih atas jawaban anda. Bisakah Anda memberikan contoh konkret kapan "Ini adalah string Java" .hashCode ()! = 586653468?
knorv
1
Tidak, maaf. Maksud saya adalah semua yang Anda uji dapat bekerja seperti yang Anda inginkan. Tapi itu masih belum ada jaminan. Jadi, jika Anda sedang mengerjakan proyek jangka pendek (katakanlah) di mana Anda memiliki kendali atas VM, dll., Maka hal di atas mungkin cocok untuk Anda. Tetapi Anda tidak dapat mengandalkannya di dunia yang lebih luas.
Brian Agnew
2
"peningkatan ke versi Java di masa depan dapat menyebabkan masalah". Pemutakhiran ke versi Java di masa depan dapat menghapus metode kode hash sepenuhnya. Atau membuatnya selalu mengembalikan 0 untuk string. Itu perubahan yang tidak cocok untuk Anda. Pertanyaannya adalah apakah Sun ^ HOracle ^ HT JCP akan menganggapnya sebagai perubahan yang melanggar dan karenanya perlu dihindari. Karena algoritma ada dalam kontrak, orang berharap mereka akan melakukannya.
Steve Jessop
@SteveJessop dengan baik, karena switchpernyataan atas string mengkompilasi ke kode yang mengandalkan kode hash tetap tertentu, perubahan pada Stringalgoritma kode hash pasti akan memecahkan kode yang ada ...
Holger
3

Hanya untuk menjawab pertanyaan Anda dan tidak melanjutkan diskusi. Implementasi Apache Harmony JDK tampaknya menggunakan algoritma yang berbeda, setidaknya terlihat sangat berbeda:

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

Jangan ragu untuk memeriksanya sendiri ...

Rene
sumber
23
Saya pikir mereka hanya bersikap dingin dan mengoptimalkannya. :) "(pengganda << 5) - pengganda" hanya 31 * pengganda, setelah semua ...
bersantai
Ok, terlalu malas untuk memeriksanya. Terima kasih!
Rene
1
Tetapi untuk membuatnya jelas dari sisi saya ... Jangan pernah mengandalkan kode hash karena kode hash adalah sesuatu yang internal.
Rene
1
apa arti variabel "offset", "hitung" dan "kode hash"? Saya kira "kode hash" digunakan sebagai nilai cache, untuk menghindari perhitungan di masa depan, dan bahwa "menghitung" adalah jumlah karakter, tetapi apa "offset"? misalkan saya ingin menggunakan kode ini sehingga akan konsisten, diberi string, apa yang harus saya lakukan untuk itu?
pengembang android
1
@androiddeveloper Sekarang ITULAH pertanyaan yang menarik - meskipun saya harus menebaknya, berdasarkan nama pengguna Anda. Dari dokumen Android sepertinya kontraknya sama: s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]Kecuali saya salah, ini karena Android menggunakan implementasi objek String oleh Sun tanpa perubahan.
Kartik Chugh
2

Jika Anda khawatir tentang perubahan dan kemungkinan VM yang tidak kompatibel, cukup salin implementasi kode hash yang ada ke kelas utilitas Anda sendiri, dan gunakan itu untuk menghasilkan kode hash Anda.

Sam Barnum
sumber
Saya akan mengatakan ini. Sementara jawaban lain menjawab pertanyaan, menulis fungsi kode hash terpisah mungkin merupakan solusi yang tepat untuk masalah knorv.
Nick
1

Kode hash akan dihitung berdasarkan nilai ASCII dari karakter dalam String.

Ini adalah implementasi di Kelas String adalah sebagai berikut

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

Tabrakan dalam kode hash tidak dapat dihindari. Misalnya, string "Ea" dan "FB" memberikan kode hash yang sama dengan 2236

Lourdes
sumber