Bir koleksiyon için hashCode yöntemi için en iyi uygulama


299

hashCode()Bir koleksiyon için yöntemin en iyi şekilde uygulanmasına nasıl karar veririz (eşittir yöntemin doğru şekilde geçersiz kılındığı varsayılarak)?


2
Java 7+ ile, sanırım Objects.hashCode(collection)mükemmel bir çözüm olmalı!
Diablo

3
@Diablo Bu sorunun cevabını hiç sanmıyorum - bu yöntem basitçe geri döner collection.hashCode()( hg.openjdk.java.net/jdk7/jdk7/jdk/file/9b8c96f96a0f/src/share/… )
cbreezier

Yanıtlar:


438

En iyi uygulama? Bu zor bir sorudur çünkü kullanım şekline bağlıdır.

Bir neredeyse tüm durumlar için makul iyi uygulama içinde önerilmiştir Josh Bloch 'ın Etkili Java Item 8 (ikinci baskı) içinde. En iyisi oraya bakmaktır çünkü yazar orada yaklaşımın neden iyi olduğunu açıklar.

Kısa versiyon

  1. A oluşturun int resultve sıfırdan farklı bir değer atayın .

  2. İçin her alanda f test equals()yönteminde, bir karma kodunu hesaplamak ctarafından:

    • F alanı a boolean: ise hesaplayın (f ? 0 : 1);
    • Alan f ise bir byte, char, shortveya int: hesapla (int)f;
    • F alanı a long: ise hesaplayın (int)(f ^ (f >>> 32));
    • F alanı a float: ise hesaplayın Float.floatToIntBits(f);
    • F alanı a ise double: Double.doubleToLongBits(f)dönüş değerini her uzun değer gibi hesaplayın ve kullanın;
    • F alanı bir nesneyse : hashCode()Yöntemin sonucunu kullanın f == null;
    • F alanı bir dizi ise : her alanı ayrı bir öğe olarak görün ve karma değerini özyinelemeli bir şekilde hesaplayın ve değerleri daha sonra açıklandığı gibi birleştirin.
  3. Karma değerini birleştirin cile result:

    result = 37 * result + c
  4. Dönüş result

Bu, çoğu kullanım durumu için karma değerlerinin uygun şekilde dağıtılmasına neden olmalıdır.


45
Evet, özellikle 37 sayısının nereden geldiğini merak ediyorum.
Kip

17
Josh Bloch'un "Etkili Java" kitabının 8. maddesini kullandım.
dmeister

40
@ dma_k Asal sayıları kullanmanın nedeni ve bu cevapta açıklanan yöntem, hesaplanan karma kodun benzersiz olmasını sağlamaktır . Asal olmayan sayılar kullanırken, bunu garanti edemezsiniz. Hangi asal rakamı seçtiğiniz önemli değil, 37 sayısı hakkında büyülü bir şey yok (çok kötü 42 asal bir sayı değil mi?)
Simon Forsberg

34
@ SimonAndréForsberg Eh, hesaplanan karma kodu her zaman benzersiz olamaz :) Bir karma koddur. Ancak bir fikrim var: asal sayının sadece bir çarpanı var, asal olmayan en az iki tane var. Bu, çarpma operatörünün aynı karmayı, yani çarpışmaya neden olması için ekstra bir kombinasyon oluşturur.
dma_k


141

Dmeister tarafından önerilen Etkili Java uygulamasından memnunsanız, kendinizinkini almak yerine bir kütüphane çağrısı kullanabilirsiniz:

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

Bu, Guava ( com.google.common.base.Objects.hashCode) veya Java 7 ( java.util.Objects.hash) içindeki standart kitaplığı gerektirir , ancak aynı şekilde çalışır.


8
Birinin bunları kullanmamak için iyi bir nedeni olmadığı sürece, bunları kesinlikle her durumda kullanmalıdır. (IMHO'nun formüle edilmesi gerektiği için daha güçlü formüle edilmesi.) Standart uygulamaları / kütüphaneleri kullanmak için tipik argümanlar geçerlidir (en iyi uygulamalar, iyi test edilmiş, daha az hataya eğilimli, vb.).
Kissaki

7
@ justin.hughey şaşkın görünüyorsun. Geçersiz kılmanız gereken tek durum hashCode, bir özelliğiniz varsa equalsve bu kitaplık yöntemlerinin tam olarak ne için tasarlandığıdır. Dokümantasyon ile ilgili davranışları oldukça açıktır equals. Bir kütüphane uygulaması, doğru bir hashCodeuygulamanın özelliklerinin ne olduğunu bilmenizi engellemediğini iddia etmez - bu kütüphaneler , geçersiz kılınan vakaların çoğuna böyle uyumlu bir uygulamayı uygulamanızı kolaylaştırırequals .
bacar

6
Java.util.Objects sınıfına bakan herhangi bir Android geliştiricisi için, yalnızca API 19'da tanıtıldı, bu yüzden KitKat veya üstünde çalıştığınızdan emin olun, aksi takdirde NoClassDefFoundError alırsınız.
Andrew Kelly

3
En iyi cevap IMO, ancak örnek java.util.Objects.hash(...)olarak guava com.google.common.base.Objects.hashCode(...)yönteminden daha JDK7 yöntemini tercih ederdim . Çoğu insanın standart kütüphaneyi ekstra bir bağımlılıktan seçeceğini düşünüyorum.
Malte Skoruppa

2
İki veya daha fazla argüman varsa ve bunlardan herhangi biri bir dizi ise, sonuç beklediğiniz gibi olmayabilir, çünkü hashCode()bir dizi için sadece onun olması gerekir java.lang.System.identityHashCode(...).
starikoff

59

Eclipse tarafından sağlanan ve oldukça iyi bir iş çıkaran işlevselliği kullanmak daha iyidir ve iş mantığınızı geliştirmek için çabalarınızı ve enerjinizi koyabilirsiniz.


4
+1 İyi bir pratik çözüm. dmeister'in çözümü daha kapsamlıdır, ancak kendim hashcode yazmaya çalıştığımda sıfırlarla başa çıkmayı unutma eğilimindeyim.
Quantum7

1
+1 Quantum7 ile aynı fikirde olmakla birlikte, Eclipse tarafından oluşturulan uygulamanın ne yaptığını ve uygulama ayrıntılarını nereden aldığını anlamanın da gerçekten iyi olduğunu söyleyebilirim.
jwir3

15
Maalesef "[IDE] tarafından sağlanan işlevsellik" içeren yanıtlar genel olarak programlama dili bağlamında gerçekten alakalı değildir. Düzinelerce IDE var ve bu soruya cevap vermiyor ... çünkü bu daha çok algoritmik belirleme ile ilgili ve doğrudan equals () uygulamasına bağlı - bir IDE'nin hakkında hiçbir şey bilmeyeceği bir şey.
Darrell Teague

57

Bu, Androidbelgelere (Wayback Machine) ve Github'daki kendi koduma bağlı olsa da, genel olarak Java için çalışacaktır. Cevabım dmeister'ın cevabının okunması ve anlaşılması çok daha kolay olan bir kod uzantısıdır .

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

}

DÜZENLE

Genellikle, geçersiz kıldığınızda hashcode(...), geçersiz kılmak da istersiniz equals(...). Bu yüzden yapacak veya zaten uygulayanlar için equals, burada Github'dan iyi bir referans var ...

@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
Android Belgeleri artık yukarıdaki kodu içermiyor, bu nedenle Wayback Machine'den
Christopher Rucinski

17

Öncelikle eşitlerin doğru uygulandığından emin olun. Gönderen bir IBM developerWorks makalesinde :

  • Simetri: İki referans için a ve b, a. (B) eşittir ve yalnızca b. Eşittir (a) ise
  • Dönüşlilik: Tüm null olmayan referanslar için, a. Eşitlikleri (a)
  • Geçiş: Eğer eşittir (b) ve eşittir (c), o zaman eşittir (c)

Ardından hashCode ile ilişkilerinin kişiye (aynı makaleden) saygı duyduğundan emin olun:

  • HashCode () ile tutarlılık: İki eşit nesne aynı hashCode () değerine sahip olmalıdır

Son olarak, iyi bir karma fonksiyonu ideal karma fonksiyonuna yaklaşmaya çalışmalıdır .


11

about8.blogspot.com, dedin ki

equals () iki nesne için true değerini döndürürse, hashCode () aynı değeri döndürmelidir. Equals () yanlış döndürürse, hashCode () farklı değerler döndürmelidir

Seninle aynı fikirde değilim. İki nesne aynı hashcode'a sahipse, eşit oldukları anlamına gelmez.

A, B'ye eşitse A.hashcode, B.hascode'a eşit olmalıdır

fakat

A.hashcode B.hascode'a eşitse, A'nın B'ye eşit olması gerektiği anlamına gelmez.


3
Eğer (A != B) and (A.hashcode() == B.hashcode())buna hash fonksiyonu çarpışması diyoruz. Çünkü hash fonksiyonunun codomain'i her zaman sonludur, ancak domain genellikle değildir. Kod bölgesi ne kadar büyük olursa, çarpışma o kadar az gerçekleşmelidir. İyi karma işlevi, belirli codomain boyutu göz önüne alındığında elde edilebilecek en büyük olasılıkla farklı nesneler için farklı karma döndürmelidir. Ancak nadiren tam olarak garanti edilebilir.
Krzysztof Jabłoński

Bu sadece yukarıdaki Gray yazı için bir yorum olmalıdır. İyi bilgi ama soruya gerçekten cevap vermiyor
Christopher Rucinski

İyi yorumlar ancak 'farklı nesneler' terimini kullanma konusunda dikkatli olun ... çünkü equals () ve dolayısıyla hashCode () uygulaması bir OO bağlamında farklı nesneler hakkında olmak zorunda değildir, ancak genellikle daha fazla etki alanı modeli temsilleriyle ilgilidir (ör. insanlar bir ülke kodunu ve ülke kimliğini paylaşırsa aynı kabul edilebilir - bunlar bir JVM'de iki farklı 'nesne' olsa da - 'eşit' olarak kabul edilir ve verilen bir hashCode'a sahiptirler ... ...
Darrell Teague

7

Tutulma kullanıyorsanız, şunları üretebilir equals()ve kullanabilirsiniz hashCode():

Kaynak -> hashCode () ve equals () oluştur.

Bu işlevi kullanarak eşitlik ve karma kod hesaplaması için hangi alanları kullanmak istediğinize karar verebilirsiniz ve Eclipse karşılık gelen yöntemleri oluşturur.


7

İyi bir uygulama var Etkili Java 'nın hashcode()ve equals()mantık Apache Commons Lang . Çıkış HashCodeBuilder ve EqualsBuilder .


1
Bu API'nin dezavantajı, her durumda eşittir ve hashcode (nesneniz değişmezse ve hash'ı önceden hesaplamazsanız), belirli durumlarda çok fazla olabilen nesne oluşturma maliyetini ödemenizdir.
James McMahon

yakın zamana kadar bu benim en sevdiğim yaklaşımdı. SharedKey OneToOne ilişkilendirmesi için bir ölçüt kullanırken StackOverFlowError koştu. Dahası, Objectssınıf Java7 üzerinde hash(Object ..args)& equals()yöntemleri sağlar . Bunlar jdk 1.7+ kullanan tüm uygulamalar için önerilir
Diablo

@Diablo Sanırım, sorun nesne grafiğinde bir döngü oldu ve sonra bazı referansları görmezden gelmeniz veya döngüyü kırmanız gerektiğinde çoğu uygulamada şansınız kalmadı (zorunludur IdentityHashMap). FWIW Ben id tabanlı bir hashCode kullanın ve tüm varlıklar için eşittir.
maaartinus

6

Diğer daha ayrıntılı cevabı (kod olarak) tamamlamak için kısa bir not:

Soruyu düşünürsek nasıl-do-i-create-in-java bir karması-tablo- ve özellikle jGuru SSS girdisi , bir hash kodu değerlendirilecektir olabilir bunun üzerine diğer bazı kriterler olduğuna inanıyoruz:

  • senkronizasyonu (algo eşzamanlı erişimi destekliyor mu, desteklemiyor mu?)
  • başarısız güvenli yineleme (algo yineleme sırasında değişen bir koleksiyon algılar mı)
  • null value (karma kodu koleksiyondaki null değeri destekliyor mu)

4

Sorunuzu doğru anlarsam, özel bir toplama sınıfınız (yani Koleksiyon arayüzünden uzanan yeni bir sınıfınız) vardır ve hashCode () yöntemini uygulamak istersiniz.

Koleksiyon sınıfınız AbstractList'i genişletiyorsa, endişelenmenize gerek yoktur, zaten tüm nesneler arasında yineleme yaparak ve hashCodes () yöntemini ekleyerek çalışan equals () ve hashCode () uygulaması vardır.

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

İstediğiniz şey belirli bir sınıf için hash kodunu hesaplamanın en iyi yoluysa, normalde eşittir yönteminde kullandığım tüm alanları işlemek için ^ (bitwise exclusive veya) operatörünü kullanırım:

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

2

@ about8: orada oldukça ciddi bir hata var.

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

aynı karma kod

muhtemelen böyle bir şey istiyorsun

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

(Bu günlerde doğrudan Java'dan hashCode'u alabilir miyim? Sanırım bazı autocasting yapıyor .. Bu durumda, toString'i atlayın, çirkin.)


3
hata about8.blogspot.com tarafından uzun cevapta - hashcode bir dize birleşiminden elde etmek, aynı dizeye kadar eklenmiş herhangi bir dize kombinasyonu için aynı olan bir hash fonksiyonu ile sizi bırakır.
SquareCog

1
Peki bu meta-tartışma ve soru ile hiç ilgili değil mi? ;-)
Huppie

1
Oldukça önemli bir kusuru olan önerilen bir cevap için bir düzeltme.
SquareCog

Bu çok sınırlı bir uygulama
Christopher Rucinski

Uygulamanız sorunu önler ve başka bir sorun ortaya çıkarır; Takas foove baryol açar hashCode. Kişisel toStringAFAIK derlemek etmez ve bu olursa, o zaman verimsiz korkunç. Böyle bir 109 * getFoo().hashCode() + 57 * getBar().hashCode()şey daha hızlı, daha basit ve gereksiz çarpışmalara neden olmaz.
maaartinus

2

Özellikle koleksiyonları istediğin gibi, diğer yanıtların henüz bahsetmediği bir yönü eklemek istiyorum: Bir HashMap, koleksiyona eklendikten sonra anahtarlarının hashcode'larını değiştirmesini beklemiyor. Tüm amacı yenmek ...


2

Apache Commons EqualsBuilder ve HashCodeBuilder üzerinde yansıma yöntemlerini kullanın .


1
Eğer bunu kullanacaksanız, yansımanın pahalı olduğunu unutmayın. Dürüst olmak gerekirse bu kodu atmak dışında bir şey için kullanmak olmaz.
James McMahon

2

Etrafta küçük bir sarmalayıcı kullanıyorum Arrays.deepHashCode(...)çünkü parametreler olarak verilen dizileri doğru şekilde işliyor

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


1

Kodumu temiz tutmama yardımcı olan Objects sınıfından Google Collections lib'den yarar yöntemleri kullanmayı tercih ederim . Çoğu zaman equalsve hashcodeyöntemler IDE'nin şablonundan yapılır, bu yüzden okunması temiz değildir.


1

Burada, üst sınıf mantıkların açıklandığı bir JDK 1.7+ yaklaşımı gösterimi yer almaktadır. Object class hashCode () hesaplı, saf JDK bağımlılığı ve ekstra manuel çalışma ile oldukça rahat görüyorum. Lütfen Objects.hash()boş toleranslı olduğunu unutmayın .

Ben herhangi bir equals()uygulama dahil değil ama gerçekte elbette buna ihtiyacınız olacak.

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

Standart uygulama zayıftır ve kullanılması gereksiz çarpışmalara yol açar. Bir düşünün

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

    ...
}

Şimdi,

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

ve

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

aynı hashCode, yani 31*(a+b) + ckullanılan çarpan List.hashCodeburada tekrar kullanılır alır. Açıkçası, çarpışmalar kaçınılmazdır, ancak gereksiz çarpışmalar üretmek sadece ... gereksizdir.

Kullanmayla ilgili oldukça akıllı bir şey yok 31. Bilgi kaybını önlemek için çarpan garip olmalıdır (herhangi bir çarpan en azından en önemli biti kaybeder, dörtlü katlar iki kaybeder, vb.). Herhangi bir tek çarpan kullanılabilir. Küçük çarpanlar daha hızlı hesaplamaya yol açabilir (JIT, vardiya ve eklemeler kullanabilir), ancak çarpmanın modern Intel / AMD'de sadece üç döngü gecikmesi olduğu göz önüne alındığında, bu hiç de önemli değildir. Küçük çarpanlar ayrıca küçük girdiler için daha fazla çarpışmaya yol açar, bu da bazen sorun olabilir.

Asalların Z / (2 ** 32) halkasında hiçbir anlamı olmadığı için asal kullanmak anlamsızdır.

Bu nedenle, rastgele seçilmiş büyük tek bir sayı kullanmanızı tavsiye ederim (asal almaktan çekinmeyin). İ86 / amd64 CPU'ları, tek bir imzalı bayta uyan işlenenler için daha kısa bir talimat kullanabildiğinden, 109 gibi çarpanlar için küçük bir hız avantajı vardır. Çarpışmaları en aza indirmek için 0x58a54cf5 gibi bir şey alın.

Farklı yerlerde farklı çarpanlar kullanmak yararlıdır, ancak muhtemelen ek işi haklı çıkarmak için yeterli değildir.


0

Karma değerleri birleştirirken, genellikle boost c ++ kitaplığında kullanılan birleştirme yöntemini kullanırım:

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

Bu, eşit bir dağıtım sağlamak için oldukça iyi bir iş çıkarır. Bu formülün nasıl çalıştığıyla ilgili bazı tartışmalar için StackOverflow yayınına bakın: boost içindeki sihirli sayı :: hash_combine

Farklı hash işlevleri hakkında iyi bir tartışma var: http://burtleburtle.net/bob/hash/doobs.html


1
Bu C ++ ile ilgili değil Java ile ilgili bir soru.
dano

-1

Basit bir sınıf için, eşittir () uygulaması tarafından kontrol edilen sınıf alanlarına dayalı olarak hashCode () yöntemini uygulamak en kolay yoldur.

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

En önemli şey hashCode () ve equals () tutarlılığını korumaktır: equals () iki nesne için true değerini döndürürse, hashCode () aynı değeri döndürmelidir. Equals () işlevi false değerini döndürürse, hashCode () yöntemi farklı değerler döndürmelidir.


1
SquareCog zaten fark etmiş gibi. Karma kodudur iki dizeleri birleştirme bir kez oluşturulur Eğer çarpışmaların kitleleri oluşturmak son derece kolaydır: ("abc"+""=="ab"+"c"=="a"+"bc"==""+"abc"). Şiddetli bir kusur. Her iki alan için hashcode'u değerlendirmek ve daha sonra bunların doğrusal kombinasyonunu hesaplamak daha iyidir (tercihen katsayılar olarak primerler kullanarak).
Krzysztof Jabłoński

@ KrzysztofJabłoński Doğru. Dahası, takas foove bargereksiz bir çarpışma da üretir.
maaartinus
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.