CodeGym /Java Blogu /Rastgele /eşittir ve hashCode yöntemleri: en iyi uygulamalar
John Squirrels
Seviye
San Francisco

eşittir ve hashCode yöntemleri: en iyi uygulamalar

grupta yayınlandı
MERHABA! Bugün Java'daki iki önemli yöntemden bahsedeceğiz: equals()ve hashCode(). Bu, onlarla ilk karşılaşmamız değil: CodeGym kursu, hakkında kısa bir dersleequals() başlar — unuttuysanız veya daha önce görmediyseniz okuyun... eşittir ve hashCode yöntemleri: en iyi uygulamalar - 1Bugünün dersinde, hakkında konuşacağız. Bu kavramlar ayrıntılı olarak Ve inan bana, konuşacak bir şeyimiz var! Ancak yenisine geçmeden önce, daha önce ele aldıklarımızı tazeleyelim :) Hatırlayacağınız gibi, iki nesneyi operatörü kullanarak karşılaştırmak genellikle kötü bir fikirdir ==çünkü ==referansları karşılaştırır. Yakın tarihli bir dersten arabalarla ilgili örneğimiz:

public class Car {

   String model;
   int maxSpeed;

   public static void main(String[] args) {

       Car car1 = new Car();
       car1.model = "Ferrari";
       car1.maxSpeed = 300;

       Car car2 = new Car();
       car2.model = "Ferrari";
       car2.maxSpeed = 300;

       System.out.println(car1 == car2);
   }
}
Konsol çıktısı:

false
Görünüşe göre iki özdeş nesne yarattık Car: iki araba nesnesinin karşılık gelen alanlarının değerleri aynıdır, ancak karşılaştırmanın sonucu yine de yanlıştır. Sebebini zaten biliyoruz: car1ve car2referansları farklı hafıza adreslerine işaret ediyor, yani eşit değiller. Ancak yine de iki referansı değil, iki nesneyi karşılaştırmak istiyoruz. Nesneleri karşılaştırmak için en iyi çözüm yöntemdir equals().

eşittir () yöntemi

Bu yöntemi sıfırdan yaratmadığımızı, bunun yerine geçersiz kıldığımızı hatırlayabilirsiniz: yöntem equals()sınıfta tanımlanır Object. Bununla birlikte, her zamanki biçiminde çok az faydası vardır:

public boolean equals(Object obj) {
   return (this == obj);
}
Bu, equals()yöntemin sınıfta nasıl tanımlandığıdır Object. Bu, bir kez daha referansların bir karşılaştırmasıdır. Neden böyle yaptılar? Peki, dilin yaratıcıları, programınızdaki hangi nesnelerin eşit kabul edildiğini ve hangilerinin eşit olmadığını nasıl biliyor? :) Bu, yöntemin ana noktasıdır equals()- bir sınıfın yaratıcısı, sınıftaki nesnelerin eşitliğini kontrol ederken hangi özelliklerin kullanılacağını belirleyen kişidir. equals()Ardından , sınıfınızdaki yöntemi geçersiz kılarsınız . "Hangi özellikleri belirler" ifadesinin anlamını tam olarak anlamadıysanız, bir örnek ele alalım. İşte bir adamı temsil eden basit bir sınıf: Man.

public class Man {

   private String noseSize;
   private String eyesColor;
   private String haircut;
   private boolean scars;
   private int dnaCode;

public Man(String noseSize, String eyesColor, String haircut, boolean scars, int dnaCode) {
   this.noseSize = noseSize;
   this.eyesColor = eyesColor;
   this.haircut = haircut;
   this.scars = scars;
   this.dnaCode = dnaCode;
}

   // Getters, setters, etc.
}
Diyelim ki iki kişinin tek yumurta ikizi mi yoksa sadece benzer mi olduğunu belirlemesi gereken bir program yazdığımızı varsayalım. Beş özelliğimiz var: burun boyutu, göz rengi, saç stili, yara izlerinin varlığı ve DNA testi sonuçları (basit olması için bunu bir tamsayı kodu olarak gösteriyoruz). Sizce bu özelliklerden hangisi programımızın tek yumurta ikizlerini tanımlamasını sağlar? eşittir ve hashCode yöntemleri: en iyi uygulamalar - 2Tabii ki, sadece bir DNA testi garanti verebilir. İki kişi aynı göz rengine, saç kesimine, buruna ve hatta yara izlerine sahip olabilir - dünyada pek çok insan var ve orada hiçbir benzerin olmadığını garanti etmek imkansız. Ancak güvenilir bir mekanizmaya ihtiyacımız var: Yalnızca bir DNA testinin sonucu doğru bir sonuca varmamızı sağlar. Bu bizim yöntemimiz için ne anlama geliyor equals()? içinde geçersiz kılmamız gerekiyorMansınıf, programımızın gereksinimleri dikkate alınarak. Yöntem, int dnaCodeiki nesnenin alanını karşılaştırmalıdır. Eşitlerse, nesneler eşittir.

@Override
public boolean equals(Object o) {
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
Gerçekten bu kadar basit mi? Tam olarak değil. Bir şeyi gözden kaçırdık. Nesnelerimiz için, nesne eşitliğini sağlamakla ilgili tek bir alan belirledik: dnaCode. Şimdi 1 değil 50 ilgili alanımız olduğunu hayal edin. Ve iki nesnenin 50 alanının tümü eşitse, o zaman nesneler eşittir. Böyle bir senaryo da mümkündür. Temel sorun, 50 alanı karşılaştırarak eşitlik sağlamanın zaman alıcı ve kaynak yoğun bir süreç olmasıdır. Şimdi , sınıfımıza Manek olarak , . Başka bir programcı sınıflarımızı kullanırsa, kolayca şöyle bir kod yazabilir: WomanMan

public static void main(String[] args) {
  
   Man man = new Man(........); // A bunch of parameters in the constructor

   Woman woman = new Woman(.........); // The same bunch of parameters.

   System.out.println(man.equals(woman));
}
Bu durumda, alan değerlerini kontrol etmek anlamsızdır: iki farklı sınıftan nesneye sahip olduğumuzu kolayca görebiliriz, yani eşit olmaları mümkün değildir! equals()Bu , karşılaştırılan nesnelerin sınıflarını karşılaştırarak yönteme bir kontrol eklememiz gerektiği anlamına gelir . Bunu düşündüğümüz iyi oldu!

@Override
public boolean equals(Object o) {
   if (getClass() != o.getClass()) return false;
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
Ama belki başka bir şeyi unuttuk? Hmm... En azından bir nesneyi kendisiyle karşılaştırmadığımızı kontrol etmeliyiz! A ve B referansları aynı bellek adresine işaret ediyorsa, bunlar aynı nesnedir ve 50 alanı karşılaştırarak zaman kaybetmemize gerek yoktur.

@Override
public boolean equals(Object o) {
   if (this == o) return true;
   if (getClass() != o.getClass()) return false;
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
Şunu kontrol etmek de zarar vermez null: hiçbir nesne eşit olamaz null. Dolayısıyla, method parametresi null ise, ek kontrollerin bir anlamı yoktur. Tüm bunları göz önünde bulundurarak, sınıf equals()için yöntemimiz Manşöyle görünür:

@Override
public boolean equals(Object o) {
   if (this == o) return true;
   if (o == null || getClass() != o.getClass()) return false;
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
Yukarıda belirtilen tüm ilk kontrolleri yapıyoruz. Günün sonunda, eğer:
  • aynı sınıftaki iki nesneyi karşılaştırıyoruz
  • ve karşılaştırılan nesneler aynı nesne değil
  • ve iletilen nesne değilnull
...sonra ilgili özelliklerin karşılaştırmasına geçeriz. Bizim için bu, dnaCodeiki nesnenin alanları anlamına gelir. Yöntemi geçersiz kılarken equals(), şu gereksinimlere uyduğunuzdan emin olun:
  1. yansıma.

    Yöntem equals(), herhangi bir nesneyi kendisiyle karşılaştırmak için kullanıldığında, doğru dönmelidir.
    Biz zaten bu şartı yerine getirdik. Yöntemimiz şunları içerir:

    
    if (this == o) return true;
    

  2. Simetri.

    Eğer a.equals(b) == true, o zaman b.equals(a)dönmelidir true.
    Yöntemimiz de bu gereksinimi karşılamaktadır.

  3. Geçişlilik.

    İki nesne üçüncü bir nesneye eşitse, o zaman birbirlerine eşit olmaları gerekir.
    Eğer a.equals(b) == trueve ise a.equals(c) == true, o zaman b.equals(c)da doğru dönmelidir.

  4. Sebat.

    Must'ın sonucu equals()yalnızca ilgili alanlar değiştirildiğinde değişir. İki nesnenin verileri değişmiyorsa, sonucu equals()her zaman aynı olmalıdır.

  5. ile eşitsizlik null.

    Herhangi bir nesne için, a.equals(null)false döndürmelidir
    Bu yalnızca bazı "yararlı öneriler" dizisi değil, Oracle belgelerinde belirtilen katı bir sözleşmedir .

hashCode() yöntemi

Şimdi yöntemden bahsedelim hashCode(). Neden gerekli? Tamamen aynı amaç için - nesneleri karşılaştırmak. Ama biz zaten var equals()! Neden başka bir yöntem? Cevap basit: performansı artırmak. Yöntem kullanılarak Java'da temsil edilen bir karma işlevi hashCode(), herhangi bir nesne için sabit uzunlukta bir sayısal değer döndürür. Java'da, hashCode()yöntem herhangi bir nesne için 32 bitlik bir sayı ( int) döndürür. equals()İki sayıyı karşılaştırmak , özellikle bu yöntem birçok alanı dikkate alıyorsa, yöntemi kullanarak iki nesneyi karşılaştırmaktan çok daha hızlıdır . Programımız nesneleri karşılaştırırsa, bunu bir hash kodu kullanarak yapmak çok daha kolaydır. Yalnızca yönteme göre nesneler eşitse, hashCode()karşılaştırmaequals()yöntem. Bu arada, hash tabanlı veri yapıları bu şekilde çalışır, örneğin tanıdık HashMap! hashCode()Yöntem gibi yöntem de geliştirici equals()tarafından geçersiz kılınır. Ve tıpkı olduğu gibi equals(), hashCode()yöntemin Oracle belgelerinde belirtilen resmi gereksinimleri vardır:
  1. İki nesne eşitse (yani equals()yöntem true değerini döndürür), o zaman aynı hash koduna sahip olmaları gerekir.

    Aksi takdirde yöntemlerimiz anlamsız olacaktır. Yukarıda belirttiğimiz gibi, hashCode()performansı artırmak için önce bir kontrol yapılmalıdır. Karma kodlar farklıysa, yöntemi nasıl tanımladığımıza göre nesneler aslında eşit olsa bile, kontrol yanlış döndürür equals().

  2. Yöntem hashCode()aynı nesne üzerinde birkaç kez çağrılırsa, her seferinde aynı sayıyı döndürmesi gerekir.

  3. Kural 1 ters yönde çalışmaz. İki farklı nesne aynı hash koduna sahip olabilir.

Üçüncü kural biraz kafa karıştırıcı. Bu nasıl olabilir? Açıklama oldukça basit. Yöntem hashCode()bir döndürür int. An, int32 bitlik bir sayıdır. Sınırlı bir değer aralığına sahiptir: -2,147,483,648 ila +2,147,483,647. Başka bir deyişle, bir için 4 milyardan biraz fazla olası değer vardır int. Şimdi, Dünya'da yaşayan tüm insanlar hakkında veri depolamak için bir program oluşturduğunuzu hayal edin. Her kişi kendi nesnesine karşılık gelecektir Person(sınıfa benzer Man). Gezegende ~7,5 milyar insan yaşıyor. Başka bir deyişle, dönüştürmek için yazdığımız algoritma ne kadar akıllı olursa olsunPersonbir int'ye nesneler, sadece yeterli sayıda olası sayıya sahip değiliz. Sadece 4,5 milyar olası int değerimiz var ama bundan çok daha fazla insan var. Bu, ne kadar uğraşırsak uğraşalım, bazı farklı kişilerin aynı hash kodlarına sahip olacağı anlamına gelir. Bu olduğunda (karma kodlar iki farklı nesne için çakışıyor) buna çarpışma diyoruz. Yöntemi geçersiz kılarken hashCode(), programcının amaçlarından biri olası çarpışma sayısını en aza indirmektir. Tüm bu kuralları hesaba katarsak, hashCode()yöntem sınıfta nasıl görünecek Person? Bunun gibi:

@Override
public int hashCode() {
   return dnaCode;
}
Şaşırmış? :) Şartlara bakarsanız hepsine uyduğumuzu göreceksiniz. Metodumuzun equals()true döndürdüğü nesneler de hashCode(). İki nesnemiz Personeşitse equals(yani, aynı dnaCode), o zaman yöntemimiz aynı sayıyı döndürür. Daha zor bir örnek ele alalım. Programımızın araba koleksiyoncuları için lüks arabaları seçmesi gerektiğini varsayalım. Koleksiyon yapmak, birçok özelliği olan karmaşık bir hobi olabilir. Belirli bir 1963 arabası, 1964 arabasından 100 kat daha pahalıya mal olabilir. 1970 model kırmızı bir araba, aynı yıl aynı markanın mavi bir arabasından 100 kat daha pahalı olabilir. eşittir ve hashCode yöntemleri: en iyi uygulamalar - 4Önceki örneğimizde, sınıfla birlikte Person, alanların çoğunu (yani insan özelliklerini) önemsiz olarak attık ve yalnızcadnaCodekarşılaştırma alanında. Artık önemsiz detayların olmadığı, çok özel bir alanda çalışıyoruz! İşte sınıfımız LuxuryAuto:

public class LuxuryAuto {

   private String model;
   private int manufactureYear;
   private int dollarPrice;

   public LuxuryAuto(String model, int manufactureYear, int dollarPrice) {
       this.model = model;
       this.manufactureYear = manufactureYear;
       this.dollarPrice = dollarPrice;
   }

   // ...getters, setters, etc.
}
Şimdi karşılaştırmalarımızda tüm alanları dikkate almalıyız. Herhangi bir hata bir müşteriye yüzbinlerce dolara mal olabilir, bu yüzden aşırı güvenli olmak daha iyi olur:

@Override
public boolean equals(Object o) {
   if (this == o) return true;
   if (o == null || getClass() != o.getClass()) return false;

   LuxuryAuto that = (LuxuryAuto) o;

   if (manufactureYear != that.manufactureYear) return false;
   if (dollarPrice != that.dollarPrice) return false;
   return model.equals(that.model);
}
Yöntemimizde equals()daha önce bahsettiğimiz tüm çekleri unutmadık. Ama şimdi nesnelerimizin üç alanını da karşılaştırıyoruz. Bu program için mutlak eşitliğe, yani her alanın eşitliğine ihtiyacımız var. Peki ya hashCode?

@Override
public int hashCode() {
   int result = model == null ? 0 : model.hashCode();
   result = result + manufactureYear;
   result = result + dollarPrice;
   return result;
}
modelSınıfımızdaki alan bir String'dir . Bu uygundur, çünkü Stringsınıf zaten hashCode()yöntemi geçersiz kılar. Alanın karma kodunu hesaplıyoruz modelve ardından diğer iki sayısal alanın toplamını ona ekliyoruz. Java geliştiricilerinin, çarpışma sayısını azaltmak için kullandıkları basit bir numarası vardır: bir karma kodu hesaplarken, ara sonucu tek bir asal değerle çarpın. En sık kullanılan sayı 29 veya 31'dir. Şu anda matematiksel inceliklere girmeyeceğiz, ancak gelecekte ara sonuçları yeterince büyük bir tek sayı ile çarpmanın, hash fonksiyonunun sonuçlarını "yaymaya" yardımcı olacağını ve, sonuç olarak, aynı hash koduna sahip nesnelerin sayısını azaltın. LuxuryAuto'daki yöntemimiz için hashCode()şöyle görünür:

@Override
public int hashCode() {
   int result = model == null ? 0 : model.hashCode();
   result = 31 * result + manufactureYear;
   result = 31 * result + dollarPrice;
   return result;
}
Bu mekanizmanın tüm karmaşıklıkları hakkında daha fazla bilgiyi StackOverflow'taki bu gönderide ve ayrıca Joshua Bloch'un yazdığı Etkili Java kitabında okuyabilirsiniz . Son olarak, bahsetmeye değer bir önemli nokta daha. equals()and yöntemini her geçersiz kıldığımızda hashCode(), bu yöntemlerde dikkate alınan belirli örnek alanları seçtik. Bu yöntemler aynı alanları dikkate alır. equals()Ancak ve içindeki farklı alanları düşünebilir miyiz hashCode()? Teknik olarak yapabiliriz. Ancak bu kötü bir fikir ve nedeni şu:

@Override
public boolean equals(Object o) {
   if (this == o) return true;
   if (o == null || getClass() != o.getClass()) return false;

   LuxuryAuto that = (LuxuryAuto) o;

   if (manufactureYear != that.manufactureYear) return false;
   return dollarPrice == that.dollarPrice;
}

@Override
public int hashCode() {
   int result = model == null ? 0 : model.hashCode();
   result = 31 * result + manufactureYear;
   result = 31 * result + dollarPrice;
   return result;
}
İşte sınıf için bizim equals()ve yöntemlerimiz . Yöntem değişmeden kaldı, ancak alanı yöntemden kaldırdık . Model, yöntem iki nesneyi karşılaştırdığında artık kullanılan bir özellik değildir. Ancak hash kodu hesaplanırken o alan yine de dikkate alınır. Sonuç olarak ne elde ederiz? İki araba yaratalım ve öğrenelim! hashCode()LuxuryAutohashCode()modelequals()equals()

public class Main {

   public static void main(String[] args) {

       LuxuryAuto ferrariGTO = new LuxuryAuto("Ferrari 250 GTO", 1963, 70000000);
       LuxuryAuto ferrariSpider = new LuxuryAuto("Ferrari 335 S Spider Scaglietti", 1963, 70000000);

       System.out.println("Are these two objects equal to each other?");
       System.out.println(ferrariGTO.equals(ferrariSpider));

       System.out.println("What are their hash codes?");
       System.out.println(ferrariGTO.hashCode());
       System.out.println(ferrariSpider.hashCode());
   }
}

Are these two objects equal to each other? 
true 
What are their hash codes? 
-1372326051 
1668702472
Hata! equals()ve yöntemleri için farklı alanlar kullanarak hashCode()onlar için kurulmuş olan sözleşmeleri ihlal ettik! Yönteme göre eşit olan iki nesne equals()aynı hash koduna sahip olmalıdır. Onlar için farklı değerler aldık. Bu tür hatalar, özellikle hash kullanan koleksiyonlarla çalışırken kesinlikle inanılmaz sonuçlara yol açabilir. Sonuç olarak, equals()ve öğesini geçersiz kıldığınızda hashCode(), aynı alanları göz önünde bulundurmalısınız. Bu ders oldukça uzundu ama bugün çok şey öğrendiniz! :) Şimdi görev çözmeye geri dönme zamanı!
Yorumlar
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION