Implementasi terbaik untuk metode hashCode untuk koleksi


299

Bagaimana kita memutuskan penerapan hashCode()metode terbaik untuk koleksi (dengan asumsi bahwa metode yang sama telah diganti dengan benar)?


2
dengan Java 7+, saya kira Objects.hashCode(collection)seharusnya menjadi solusi yang sempurna!
Diablo

3
@Diablo Saya tidak berpikir yang menjawab pertanyaan sama sekali - metode itu hanya kembali collection.hashCode()( hg.openjdk.java.net/jdk7/jdk7/jdk/file/9b8c96f96a0f/src/share/… )
cbreezier

Jawaban:


438

Implementasi terbaik? Itu adalah pertanyaan yang sulit karena itu tergantung pada pola penggunaan.

A untuk hampir semua kasus, implementasi yang baik dan wajar diusulkan dalam Josh Bloch 's Java yang Efektif pada Butir 8 (edisi kedua). Yang terbaik adalah mencarinya di sana karena penulis menjelaskan di sana mengapa pendekatannya bagus.

Versi singkat

  1. Buat int resultdan tetapkan nilai yang bukan nol .

  2. Untuk setiap bidang yang f diuji dalam equals()metode ini, hitung kode hash cdengan:

    • Jika bidang f adalah boolean: hitung (f ? 0 : 1);
    • Jika bidang f adalah byte, char, shortatau int: menghitung (int)f;
    • Jika bidang f adalah long: hitung (int)(f ^ (f >>> 32));
    • Jika bidang f adalah float: hitung Float.floatToIntBits(f);
    • Jika bidang f adalah double: menghitung Double.doubleToLongBits(f)dan menangani nilai pengembalian seperti setiap nilai panjang;
    • Jika bidang f adalah objek : Gunakan hasil dari hashCode()metode atau 0 jika f == null;
    • Jika bidang f adalah array : lihat setiap bidang sebagai elemen terpisah dan hitung nilai hash secara rekursif dan gabungkan nilai-nilai seperti yang dijelaskan berikutnya.
  3. Gabungkan nilai hash cdengan result:

    result = 37 * result + c
  4. Kembali result

Ini harus menghasilkan distribusi nilai hash yang tepat untuk sebagian besar situasi penggunaan.


45
Ya saya sangat ingin tahu tentang dari mana nomor 37 berasal.
Kip

17
Saya menggunakan item 8 dari buku "Java Efektif" Josh Bloch.
tuan

39
@dma_k Alasan menggunakan bilangan prima dan metode yang dijelaskan dalam jawaban ini adalah untuk memastikan bahwa kode hash yang dikomputasi akan menjadi unik . Saat menggunakan nomor non-prima, Anda tidak dapat menjamin ini. Tidak masalah nomor utama mana yang Anda pilih, tidak ada yang ajaib tentang nomor 37 (terlalu buruk 42 bukan angka utama, eh?)
Simon Forsberg

34
@ SimonAndréForsberg Yah, kode hash yang dikomputasi tidak selalu unik :) Adalah kode hash. Namun saya mendapat ide: bilangan prima hanya memiliki satu pengganda, sedangkan non-prima memiliki setidaknya dua. Itu menciptakan kombinasi ekstra untuk operator perkalian untuk menghasilkan hash yang sama, yaitu menyebabkan tabrakan.
dma_k


140

Jika Anda senang dengan implementasi Java Efektif yang direkomendasikan oleh dmeister, Anda dapat menggunakan panggilan perpustakaan alih-alih memutar sendiri:

@Override
public int hashCode() {
    return Objects.hashCode(this.firstName, this.lastName);
}

Ini membutuhkan Guava ( com.google.common.base.Objects.hashCode) atau pustaka standar di Java 7 ( java.util.Objects.hash) tetapi bekerja dengan cara yang sama.


8
Kecuali jika seseorang memiliki alasan yang kuat untuk tidak menggunakan ini, orang pasti harus menggunakan ini dalam hal apa pun. (Merumuskannya lebih kuat, karena IMHO harus dirumuskan.) Argumen khas untuk menggunakan implementasi standar / perpustakaan berlaku (praktik terbaik, teruji dengan baik, lebih sedikit rawan kesalahan, dll).
Kissaki

7
@ justin.hughey kau sepertinya bingung. Satu-satunya kasus yang Anda harus timpa hashCodeadalah jika Anda memiliki kebiasaan equals, dan itulah tepatnya metode perpustakaan ini dirancang untuk. Dokumentasi ini cukup jelas tentang perilaku mereka terkait equals. Implementasi perpustakaan tidak mengklaim untuk membebaskan Anda dari mengetahui apa karakteristik hashCodeimplementasi yang benar - perpustakaan ini memudahkan Anda untuk mengimplementasikan implementasi yang sesuai untuk sebagian besar kasus di mana equalsditimpa.
bacar

6
Untuk setiap pengembang Android yang melihat kelas java.util.Objects, itu hanya diperkenalkan di API 19, jadi pastikan Anda menjalankan di KitKat atau di atas, jika tidak Anda akan mendapatkan NoClassDefFoundError.
Andrew Kelly

3
Jawaban terbaik IMO, meskipun dengan contoh saya lebih suka memilih java.util.Objects.hash(...)metode JDK7 daripada com.google.common.base.Objects.hashCode(...)metode jambu biji . Saya pikir kebanyakan orang akan memilih perpustakaan standar daripada ketergantungan ekstra.
Malte Skoruppa

2
Jika ada dua argumen atau lebih dan jika salah satu dari mereka adalah array, hasilnya mungkin bukan yang Anda harapkan karena hashCode()untuk array hanya itu java.lang.System.identityHashCode(...).
starikoff

59

Lebih baik menggunakan fungsi yang disediakan oleh Eclipse yang melakukan pekerjaan yang cukup bagus dan Anda dapat menempatkan upaya dan energi Anda dalam mengembangkan logika bisnis.


4
+1 Solusi praktis yang bagus. Solusi dmeister lebih komprehensif, tetapi saya cenderung lupa untuk menangani null ketika saya mencoba menulis kode hash sendiri.
Quantum7

1
+1 Setuju dengan Quantum7, tapi saya akan mengatakan itu juga sangat bagus untuk memahami apa yang dilakukan implementasi Eclipse, dan dari mana ia mendapatkan detail implementasinya.
jwir3

15
Maaf tetapi jawaban yang melibatkan "fungsionalitas yang disediakan oleh [beberapa IDE]" tidak benar-benar relevan dalam konteks bahasa pemrograman secara umum. Ada puluhan IDE dan ini tidak menjawab pertanyaan ... yaitu karena ini lebih tentang penentuan algoritmik dan terkait langsung dengan implementasi equals () - sesuatu yang tidak diketahui oleh IDE.
Darrell Teague

57

Meskipun ini terkait dengan Androiddokumentasi (Mesin Wayback) dan kode saya sendiri di Github , ini akan berfungsi untuk Java secara umum. Jawaban saya adalah perpanjangan dari Jawaban dmeister dengan hanya kode yang lebih mudah dibaca dan dimengerti.

@Override 
public int hashCode() {

    // Start with a non-zero constant. Prime is preferred
    int result = 17;

    // Include a hash for each field.

    // Primatives

    result = 31 * result + (booleanField ? 1 : 0);                   // 1 bit   » 32-bit

    result = 31 * result + byteField;                                // 8 bits  » 32-bit 
    result = 31 * result + charField;                                // 16 bits » 32-bit
    result = 31 * result + shortField;                               // 16 bits » 32-bit
    result = 31 * result + intField;                                 // 32 bits » 32-bit

    result = 31 * result + (int)(longField ^ (longField >>> 32));    // 64 bits » 32-bit

    result = 31 * result + Float.floatToIntBits(floatField);         // 32 bits » 32-bit

    long doubleFieldBits = Double.doubleToLongBits(doubleField);     // 64 bits (double) » 64-bit (long) » 32-bit (int)
    result = 31 * result + (int)(doubleFieldBits ^ (doubleFieldBits >>> 32));

    // Objects

    result = 31 * result + Arrays.hashCode(arrayField);              // var bits » 32-bit

    result = 31 * result + referenceField.hashCode();                // var bits » 32-bit (non-nullable)   
    result = 31 * result +                                           // var bits » 32-bit (nullable)   
        (nullableReferenceField == null
            ? 0
            : nullableReferenceField.hashCode());

    return result;

}

EDIT

Biasanya, saat Anda menimpa hashcode(...), Anda juga ingin menimpa equals(...). Jadi bagi yang mau atau sudah menerapkan equals, berikut ini adalah referensi yang bagus dari Github saya ...

@Override
public boolean equals(Object o) {

    // Optimization (not required).
    if (this == o) {
        return true;
    }

    // Return false if the other object has the wrong type, interface, or is null.
    if (!(o instanceof MyType)) {
        return false;
    }

    MyType lhs = (MyType) o; // lhs means "left hand side"

            // Primitive fields
    return     booleanField == lhs.booleanField
            && byteField    == lhs.byteField
            && charField    == lhs.charField
            && shortField   == lhs.shortField
            && intField     == lhs.intField
            && longField    == lhs.longField
            && floatField   == lhs.floatField
            && doubleField  == lhs.doubleField

            // Arrays

            && Arrays.equals(arrayField, lhs.arrayField)

            // Objects

            && referenceField.equals(lhs.referenceField)
            && (nullableReferenceField == null
                        ? lhs.nullableReferenceField == null
                        : nullableReferenceField.equals(lhs.nullableReferenceField));
}

1
Dokumentasi Android sekarang tidak termasuk kode di atas lagi, jadi di sini adalah versi cache dari Wayback Machine - Dokumentasi Android (07 Feb 2015)
Christopher Rucinski

17

Pertama-tama pastikan bahwa persamaan diterapkan dengan benar. Dari artikel IBM DeveloperWorks :

  • Simetri: Untuk dua referensi, a dan b, a.equals (b) jika dan hanya jika b.equals (a)
  • Refleksivitas: Untuk semua referensi yang bukan nol, a.equals (a)
  • Transitivitas: Jika a.equals (b) dan b.equals (c), maka a.equals (c)

Kemudian pastikan bahwa hubungannya dengan hashCode menghormati kontak (dari artikel yang sama):

  • Konsistensi dengan kode hash (): Dua objek yang sama harus memiliki nilai kode hash () yang sama

Akhirnya fungsi hash yang baik harus berusaha untuk mendekati fungsi hash yang ideal .


11

about8.blogspot.com, katamu

jika equals () mengembalikan true untuk dua objek, maka hashCode () harus mengembalikan nilai yang sama. Jika equals () mengembalikan false, maka hashCode () harus mengembalikan nilai yang berbeda

Saya tidak bisa setuju dengan Anda. Jika dua objek memiliki kode hash yang sama, itu tidak harus berarti bahwa mereka sama.

Jika A sama dengan B maka A.hashcode harus sama dengan B.hascode

tapi

jika A.hashcode sama dengan B.hascode, itu tidak berarti bahwa A harus sama dengan B


3
Jika (A != B) and (A.hashcode() == B.hashcode()), itulah yang kami sebut tabrakan fungsi hash. Itu karena kode fungsi hash selalu terbatas, sedangkan domainnya biasanya tidak. Semakin besar kode domain, semakin jarang tabrakan terjadi. Fungsi hash yang baik harus mengembalikan hash yang berbeda untuk objek yang berbeda dengan kemungkinan terbesar yang dapat dicapai mengingat ukuran kode domain tertentu. Ini jarang bisa sepenuhnya dijamin.
Krzysztof Jabłoński

Ini seharusnya hanya komentar untuk posting di atas untuk Gray. Informasi yang baik tetapi tidak benar-benar menjawab pertanyaan
Christopher Rucinski

Komentar yang bagus tetapi berhati-hatilah dalam menggunakan istilah 'objek berbeda' ... karena equals () dan implementasi hashCode () tidak harus mengenai objek yang berbeda dalam konteks OO tetapi biasanya lebih tentang representasi model domain mereka (misalnya, dua orang dapat dianggap sama jika mereka berbagi kode negara dan ID negara - meskipun ini mungkin dua 'objek' yang berbeda dalam JVM - mereka dianggap 'sama' dan memiliki kode hash yang diberikan) ...
Darrell Teague

7

Jika Anda menggunakan gerhana, Anda dapat membuat equals()dan hashCode()menggunakan:

Sumber -> Hasilkan kode hash () dan equals ().

Dengan menggunakan fungsi ini, Anda dapat memutuskan bidang mana yang ingin Anda gunakan untuk perhitungan kode persamaan dan hash, dan Eclipse menghasilkan metode yang sesuai.


7

Ada implementasi yang baik dari Jawa Efektif 's hashcode()dan equals()logika dalam Apache Commons Lang . Lihat HashCodeBuilder dan EqualsBuilder .


1
Kelemahan dari API ini adalah Anda membayar biaya konstruksi objek setiap kali Anda memanggil sama dan kode hash (kecuali objek Anda tidak dapat diubah dan Anda precompute hash), yang dapat banyak dalam kasus-kasus tertentu.
James McMahon

ini adalah pendekatan favorit saya, hingga saat ini. Saya telah berlari ke StackOverFlowError saat menggunakan kriteria untuk asosiasi SharedKey OneToOne. Terlebih lagi, Objectskelas menyediakan hash(Object ..args)& equals()metode dari Java7 pada. Ini direkomendasikan untuk aplikasi apa pun yang menggunakan jdk 1.7+
Diablo

@Diablo Saya kira, masalah Anda adalah siklus dalam grafik objek dan kemudian Anda kurang beruntung dengan sebagian besar implementasi karena Anda perlu mengabaikan beberapa referensi atau untuk memutus siklus (mandat sebuah IdentityHashMap). FWIW Saya menggunakan kode hash berbasis id dan sama dengan untuk semua entitas.
maaartinus

6

Hanya catatan singkat untuk melengkapi jawaban lain yang lebih terperinci (dalam hal kode):

Jika saya mempertimbangkan pertanyaan bagaimana-membuat-saya-membuat-tabel- has -di-java dan terutama entri FAQ jGuru , saya percaya beberapa kriteria lain yang dapat dinilai oleh kode hash adalah:

  • sinkronisasi (apakah algo mendukung akses bersamaan atau tidak)?
  • gagal iterasi aman (apakah algo mendeteksi koleksi yang berubah selama iterasi)
  • nilai nol (apakah kode hash mendukung nilai nol dalam koleksi)

4

Jika saya memahami pertanyaan Anda dengan benar, Anda memiliki kelas koleksi khusus (yaitu kelas baru yang meluas dari antarmuka Koleksi) dan Anda ingin menerapkan metode hashCode ().

Jika kelas koleksi Anda memperluas AbstractList, maka Anda tidak perlu khawatir tentang hal itu, sudah ada implementasi equals () dan hashCode () yang berfungsi dengan mengiterasi semua objek dan menambahkan kode hash mereka () bersama-sama.

   public int hashCode() {
      int hashCode = 1;
      Iterator i = iterator();
      while (i.hasNext()) {
        Object obj = i.next();
        hashCode = 31*hashCode + (obj==null ? 0 : obj.hashCode());
      }
  return hashCode;
   }

Sekarang jika yang Anda inginkan adalah cara terbaik untuk menghitung kode hash untuk kelas tertentu, saya biasanya menggunakan operator ^ (bitwise eksklusif atau) untuk memproses semua bidang yang saya gunakan dalam metode equals:

public int hashCode(){
   return intMember ^ (stringField != null ? stringField.hashCode() : 0);
}

2

@ about8: ada bug yang cukup serius di sana.

Zam obj1 = new Zam("foo", "bar", "baz");
Zam obj2 = new Zam("fo", "obar", "baz");

kode hash yang sama

Anda mungkin menginginkan sesuatu seperti

public int hashCode() {
    return (getFoo().hashCode() + getBar().hashCode()).toString().hashCode();

(Bisakah Anda mendapatkan kode hash langsung dari int di Jawa hari ini? Saya pikir ini melakukan autocasting .. jika itu masalahnya, lewati toString, itu jelek.)


3
bug ada dalam jawaban panjang sekitar about8.blogspot.com - mendapatkan kode hash dari rangkaian string membuat Anda memiliki fungsi hash yang sama untuk setiap kombinasi string yang menambahkan hingga string yang sama.
SquareCog

1
Jadi ini meta-diskusi dan tidak terkait dengan pertanyaan sama sekali? ;-)
Huppie

1
Ini merupakan koreksi terhadap jawaban yang diajukan yang memiliki cacat yang cukup signifikan.
SquareCog

Ini adalah implementasi yang sangat terbatas
Christopher Rucinski

Implementasi Anda menghindari masalah dan memperkenalkan yang lain; Bertukar foodan barmengarah ke hal yang sama hashCode. Anda toStringAFAIK tidak mengkompilasi, dan jika tidak, maka itu mengerikan tidak efisien. Sesuatu seperti 109 * getFoo().hashCode() + 57 * getBar().hashCode()lebih cepat, lebih sederhana dan tidak menghasilkan benturan yang tidak perlu.
maaartinus

2

Saat Anda secara spesifik meminta koleksi, saya ingin menambahkan aspek yang belum dijawab oleh jawaban lain: HashMap tidak mengharapkan kunci mereka untuk mengubah kode hash mereka begitu ditambahkan ke koleksi. Akan mengalahkan seluruh tujuan ...



2

Saya menggunakan pembungkus kecil di sekitar Arrays.deepHashCode(...)karena menangani array yang disediakan sebagai parameter dengan benar

public static int hash(final Object... objects) {
    return Arrays.deepHashCode(objects);
}


1

Saya lebih suka menggunakan metode utilitas dari Google Koleksi Google lib dari Objek kelas yang membantu saya menjaga kode saya bersih. Sangat sering equalsdan hashcodemetode dibuat dari template IDE, sehingga tidak bersih untuk dibaca.


1

Berikut ini adalah demonstrasi pendekatan JDK 1.7+ lainnya dengan logika superclass. Saya melihatnya cukup meyakinkan dengan kelas Object hashCode () dicatat, ketergantungan JDK murni dan tidak ada pekerjaan manual tambahan. Harap dicatat Objects.hash()tidak ada toleransi.

Saya belum memasukkan equals()implementasi tetapi pada kenyataannya Anda tentu saja akan membutuhkannya.

import java.util.Objects;

public class Demo {

    public static class A {

        private final String param1;

        public A(final String param1) {
            this.param1 = param1;
        }

        @Override
        public int hashCode() {
            return Objects.hash(
                super.hashCode(),
                this.param1);
        }

    }

    public static class B extends A {

        private final String param2;
        private final String param3;

        public B(
            final String param1,
            final String param2,
            final String param3) {

            super(param1);
            this.param2 = param2;
            this.param3 = param3;
        }

        @Override
        public final int hashCode() {
            return Objects.hash(
                super.hashCode(),
                this.param2,
                this.param3);
        }
    }

    public static void main(String [] args) {

        A a = new A("A");
        B b = new B("A", "B", "C");

        System.out.println("A: " + a.hashCode());
        System.out.println("B: " + b.hashCode());
    }

}

1

Implementasi standar lemah dan menggunakannya menyebabkan tabrakan yang tidak perlu. Bayangkan a

class ListPair {
    List<Integer> first;
    List<Integer> second;

    ListPair(List<Integer> first, List<Integer> second) {
        this.first = first;
        this.second = second;
    }

    public int hashCode() {
        return Objects.hashCode(first, second);
    }

    ...
}

Sekarang,

new ListPair(List.of(a), List.of(b, c))

dan

new ListPair(List.of(b), List.of(a, c))

memiliki yang sama hashCode, yaitu 31*(a+b) + csebagai pengganda yang digunakan untukList.hashCode kembali di sini. Jelas, tabrakan tidak dapat dihindari, tetapi menghasilkan tabrakan yang tidak perlu hanya ... tidak perlu.

Tidak ada yang secara substansial pintar dalam menggunakan 31. Pengganda harus ganjil untuk menghindari kehilangan informasi (pengganda genap mana pun kehilangan setidaknya bit yang paling signifikan, kelipatan empat kehilangan dua, dll.). Pengganda ganjil dapat digunakan. Pengganda kecil dapat menyebabkan komputasi lebih cepat (JIT dapat menggunakan shift dan penambahan), tetapi mengingat bahwa perkalian memiliki latensi hanya tiga siklus pada Intel / AMD modern, ini hampir tidak masalah. Pengganda kecil juga menyebabkan lebih banyak tabrakan untuk input kecil, yang terkadang menjadi masalah.

Menggunakan prime tidak ada gunanya karena bilangan prima tidak memiliki makna di cincin Z / (2 ** 32).

Jadi, saya akan merekomendasikan menggunakan nomor ganjil besar yang dipilih secara acak (jangan ragu untuk mengambil prime) Karena CPU i86 / amd64 dapat menggunakan instruksi yang lebih pendek untuk pemasangan operan dalam byte bertanda tunggal, ada keunggulan kecepatan kecil untuk pengganda seperti 109. Untuk meminimalkan benturan, ambil sesuatu seperti 0x58a54cf5.

Menggunakan pengganda yang berbeda di tempat yang berbeda itu membantu, tetapi mungkin tidak cukup untuk membenarkan pekerjaan tambahan.


0

Saat menggabungkan nilai hash, saya biasanya menggunakan metode menggabungkan yang digunakan di pustaka boost c ++, yaitu:

seed ^= hasher(v) + 0x9e3779b9 + (seed<<6) + (seed>>2);

Ini melakukan pekerjaan yang cukup baik untuk memastikan distribusi yang merata. Untuk beberapa diskusi tentang cara kerja rumus ini, lihat posting StackOverflow: Nomor ajaib di boost :: hash_combine

Ada diskusi bagus tentang berbagai fungsi hash di: http://burtleburtle.net/bob/hash/doobs.html


1
Ini adalah pertanyaan tentang Java, bukan C ++.
dano

-1

Untuk kelas sederhana, seringkali paling mudah untuk mengimplementasikan kode hash () berdasarkan bidang kelas yang diperiksa oleh implementasi equals ().

public class Zam {
    private String foo;
    private String bar;
    private String somethingElse;

    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }

        if (obj == null) {
            return false;
        }

        if (getClass() != obj.getClass()) {
            return false;
        }

        Zam otherObj = (Zam)obj;

        if ((getFoo() == null && otherObj.getFoo() == null) || (getFoo() != null && getFoo().equals(otherObj.getFoo()))) {
            if ((getBar() == null && otherObj. getBar() == null) || (getBar() != null && getBar().equals(otherObj. getBar()))) {
                return true;
            }
        }

        return false;
    }

    public int hashCode() {
        return (getFoo() + getBar()).hashCode();
    }

    public String getFoo() {
        return foo;
    }

    public String getBar() {
        return bar;
    }
}

Yang paling penting adalah menjaga agar kode hash () dan equals () konsisten: jika equals () mengembalikan nilai true untuk dua objek, maka kode hash () harus mengembalikan nilai yang sama. Jika equals () mengembalikan false, maka hashCode () harus mengembalikan nilai yang berbeda.


1
Seperti SquareCog sudah perhatikan. Jika kode hash yang dihasilkan sekali dari gabungan dari dua string adalah sangat mudah untuk menghasilkan massa tabrakan: ("abc"+""=="ab"+"c"=="a"+"bc"==""+"abc"). Ini cacat parah. Akan lebih baik untuk mengevaluasi kode hash untuk kedua bidang dan kemudian menghitung kombinasi linear dari keduanya (lebih disukai menggunakan bilangan prima sebagai koefisien).
Krzysztof Jabłoński

@ KrzysztofJabłoński Benar. Selain itu, bertukar foodan barmenghasilkan tabrakan yang tidak perlu juga.
maaartinus
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.