post-images/overriding-hashcode/cover.jpg

Overriding hashCode()

Selam dostum yine ben 😎 ve yeni ünitenin ikinci yazısıyla devam ediyoruz. Bu yazıda Effective Java kitabının ikinci ünitesi olan Methods Common to All Objects'in ikinci maddesini ele alacağız.

Bu madde içerisinde Object sınıfı içerisinde yer alan hashcode() metodunun nasıl override edileceğini, hangi koşullarda override edilmesi gerektiğini ve dikkat etmemiz gereken kritik hatalara değineceğim.

Object sınıfımızı bir ziyaret edelim ne varmış bu hashCode() içerisinde.

/**  
 * Returns a hash code value for the object. This method is 
 * supported for the benefit of hash tables such as those provided by 
 * {@link java.util.HashMap}.
 */
public native int hashCode();

Object sınıfı içerisindeki bu metoda baktığımda herhangi bir implementasyonunun olmadığını görüyorum. Peki ne yapıyor bu metot? Aslında kendi sınıfım için bir hash code üretmek istediğim durumda bu metodu override edip nesnem için bir hash code üretmem gerekiyor. Peki hashCode() metodunu ne zaman kullanmam gerekiyor. equals() metodunun override edilmesi durumunda muhakkak hashCode'unda override edilmesi gerekiyor. Peki ne alaka, neden böyle bir gereksinim var? Diyorsan merak etme daha detaylı inceleyeceğim. İncelemeye geçmeden önce Overriding equals() yazıma göz atmak isteyebilirsin.

equals() metodu gibi hashCode() metodunun da kendine ait sözleşmesi mevcut hashCode() implementasyonu yaparken bu maddeleri ihmal etmemem gerekiyor. Nedenini sözleşme maddelerinden sonra açıklayacağım.

  • Bir nesne içerisinde override ettiğim hashCode() metodu her çağrılmada aynı değeri üretmelidir. Bu durum equals() içerisindeki kontrollerin değişmediği sürece sağlanması gerekir.
  • equals() metoduna göre iki nesne birbirine eşitse her iki nesneninde hashCode() metodu aynı tam sayı değerini üretmelidir.
  • İkinci maddenin aksine equals() metoduna göre iki nesne birbirine eşit değilse, her iki nesneye ait hashCode() metodunun farklı tam sayı üretme gerekliliği yoktur. Fakat farklı sayılar üretmesi hashTable performansını arttırır.

Tam bu aşamada hash code'un ne olduğundan bahsetmek gerekirse HashMap, HashSet, HashTable gibi veri yapıları bahsi geçen hashCode() ile hash code hesaplarken nesnenin o anki bellekte bulunduğu konumu referans alır.

Hızlıca bir kural ihlali ile devam edeyim. Eğer equals() metodunu implement etmiş fakat hashCode() metoduna hiç dokunmamış isem; equals() metoduna göre eşit olan nesnelerimi HashMap, HashSet, HashTable gibi veri yapılarında kullandığımda birbirinden farklı iki nesne olarak görecektir. Bu durum benim ikinci maddemi ihlal ettiğimi gösterir.

Hemen bir örnek ile pekiştirmeni sağlayayım:

public class PhoneNumber {

    private int areaCode;
    private int prefix;
    private int lineNumber;

    public PhoneNumber(int areaCode, int prefix, int lineNumber) {
        this.areaCode = areaCode;
        this.prefix = prefix;
        this.lineNumber = lineNumber;
    }

    @Override
    public boolean equals(Object o) {
        if (o == this)
            return true;
        if (!(o instanceof PhoneNumber))
            return false;

        PhoneNumber pn = (PhoneNumber) o;
        return pn.lineNumber == lineNumber &&
            pn.prefix == prefix &&
            pn.areaCode == areaCode;
    }

    // No hashCode() method!
}

Yukarıdaki sınıfımız equals metodu anlamında gayet tamam gibi duruyor. Peki ya hashCode() olmadığına göre burada ne gibi bir sorun var? Sınıf için instance'lar üretmeyi deneyelim...

Map<PhoneNumber, String> map = new HashMap<>();  
map.put(new PhoneNumber(707, 867, 5309), "Jenny");

Tamamadır. Key = new PhoneNumber(707, 867, 5309), Value = "Jenny" olacak şekilde elimde bir map entrysi mevcut değil mi?

map.get(new PhoneNumber(707, 867, 5309));

Veee "Jenny" cevabını görürüz... Demek isterdim ama malesef null dönecektir. equals() metodunda bu iki nesne birbirine eşittir new PhoneNumber(707, 867, 5309).equals(new PhoneNumber(707, 867, 5309)) kontrolü true değeri dönecektir. equals() mantıksal değer kontrolü yaparken hashCode()'un bellek referansına baktığı unutulmamalıdır.

Bu sorunu çözmenin yolu tabii ki yazıda incelediğimiz konu ile aynı 😊 Peki hashCode() metodunu sözleşme maddelerine uyacak şekilde nasıl yazabiliriz. Bu aşamada sözleşme maddelerine uymak pek zor değil fakat birbirinden farklı hash code üretmek epey bir zordur. Hatta aşağıdaki örnek sözleşmelere uyar fakat çok kötü bir hash code implementasyonudur.

// The worst possible legal hashCode implementation - never use!
@Override public int hashCode() { return 53; }

✅ Nesnemizin hashCode() metodu her çağrıldığında aynı değeri mi dönüyor?
equals() içerisinde kontrol edilen eşit nesnelerin hash code’ları aynı mı?
equals() içerisinde kontrol edilen eşit olmayan nesnelerin hash codeları aynı olmak zorunda değil.

Gördüğün gibi sözleşmenin her bir maddesi gerçekleştiriliyor. Fakat iyi bir hash code ürettim diyebilmek için sahip olduğu değerler büyük oranda birbirinden farklı olmalıdır. Farklı olmadığı durumda büyük performans kayıplarına neden olur. Daha önce sözleşmenin 3. maddesinde bahsettiğim HashTableLinkedList gibi çalıştırarak 4 kat yavaş çalışmasına neden olmaktadır. Örnek birkaç hashCode() implementasyonu şu şekilde;

  • Standart hashCode() implementasyonu (en iyi performans):
@Override  
public int hashCode() {
    int hash = 7; 
    hash = 31 * hash + (int) id; 
    hash = 31 * hash + (name == null ? 0 : name.hashCode()); 
    hash = 31 * hash + (email == null ? 0 : email.hashCode()); 
	
    return hash; 
}
  • Nesnemin özelliklerini de kullanabilrim:
@Override  
public int hashCode() {
    return (int) id * name.hashCode() * email.hashCode(); 
}

Bunların alternatifi olarak @AutoValue annotation'ını kullanabilirsiniz. Sizler için otomatik değer üretecektir.

equals() metodunu kendi sınıfı için override eden arkadaşlarımın hashCode() konusunda da aynı hassasiyeti göstermeleri gerektiği konusunda bence hem fikirizdir değil mi?

Sonraki yazıda görüşmek üzere 🖖🏼

JavaEffective JavaMethods Common to All ObjectshashCode()OverridingSoftware ContractAutoValue