HashMap, equals ve hashCode
30 saniyede özet
HashMap bir şeyi iki soruyla bulur: hangi kutuya bakayım (hashCode) ve bu kutudakilerden hangisi (equals). İkisini birlikte yazmazsan koyduğunu bulamazsın, anahtarı sonradan değiştirirsen de kaybedersin.
Anahtarla bir şey koydun, aynı anahtarla istedin ve null geldi. Suçlu çoğu zaman HashMap değil, iki küçük metodun iş bölümünü bozan kendi sınıfındır.
-
Bayt:
map.put(ali, veri)yaptım. Sonramap.get(ali)dedim ve...null?! -
Sen: Ama aynı anahtar! equals bile true diyor.
-
Bayt: HashMap önce kapı numarasına bakar, sonra kapıdaki isme. Sen hangisini unuttun?
-
Bayt: hashCode ile equals'ı birlikte yazan kazanır. Hadi kapıları çalalım.
İş bölümü
hashCodeNesneyi bir tamsayıya indiren metot. Eşit nesneler aynı sayıyı vermek zorundadır; farklı nesnelerin aynı sayıyı vermesi serbesttir.Sözlükte gör → hangi bucketHashMap'in içindeki dizi gözlerinden biri. Bir anahtar hangi göze düşeceğini hashCode'undan öğrenir.Aynı göze düşen anahtarlar bir zincir (Java 8'den beri kalabalıklaşınca ağaç) oluşturur; equals onların arasından doğrusunu seçer.Sözlükte gör → bakılacağını belirler. equals() ise o bucket’ın içinde hangi girdinin doğru girdi olduğunu.
İkisi birbirinin yerine geçmez: hashCode bir adres, equals bir kimlik kontrolüdür.
Bu yüzden sözleşme tek yönlüdür:
a.equals(b)isea.hashCode() == b.hashCode()olmak zorundadır. Tersi zorunlu değildir: aynı hashCode’a sahip iki nesne eşit olmayabilir — buna hash collisionİki farklı anahtarın aynı bucket'a düşmesi. Hata değil, normaldir — HashMap bunu zincirleyerek çözer.Sözlükte gör → denir ve normaldir.
Kafam karıştı, daha basit anlat
hashCode hangi kapıya gideceğini söyler, equals kapıyı açanın doğru kişi olup olmadığını. İkisi aynı kişi için aynı cevabı vermezse, doğru kapıya hiç varamazsın.
`map.get(anahtar)` çağrısı doğru girdiyi hangi sırayla bulur?
`equals`'ı override edip `hashCode`'u override etmezseniz HashMap'te ne olur?
Satır satır: anahtar kaybolduğunda
En tehlikeli HashMap hatası burada. Girdi silinmez, taşınmaz, hata da vermez — sadece bir daha bulunamaz.
`key.setName("veli")` çağrıldıktan sonra `map.get(key)` ne döner? Cevabı göster
null döner. Ama map.size() hâlâ 1 der — girdi eski bucket’ında duruyor, sadece artık orada aranmıyor.
Adım adım oku
- hashCode, anahtarı bir kapı numarasına (bucket) çevirir: ali için 1.
- equals, o kapının arkasındaki girdinin gerçekten aranan anahtar olup olmadığına bakar.
- Anahtarın adı veli olunca hashCode da değişir ve yeni numara 3 olur.
- 3 numaralı kapı boştur, get null döner. Girdi hâlâ 1 numarada duruyor ve size onu saymaya devam ediyor.
Kaybolan girdi
var key = new User("ali");var map = new HashMap<User, String>();map.put(key, "veri"); System.out.println(map.get(key)); key.setName("veli"); System.out.println(map.get(key));System.out.println(map.size());Debug
main User'ın hashCode'u ad alanından türüyor. Şu an "ali".
- key.name
- = ali
- key.hashCode()
- = 96865
Sol/sağ ok tuşlarıyla da gezebilirsin.
Kural: HashMap anahtarları immutableOluşturulduktan sonra içi değişmeyen nesne. Anahtar olarak güvenlidir, çünkü hashCode'u hiç değişmez.Sözlükte gör → olmalıdır. String, Integer ve record’ların iyi anahtar olmasının sebebi budur.
// equals ve hashCode otomatik, alanlar finalpublic record UserId(String value) {}Kafam karıştı, daha basit anlat
Bir kutuyu adının baş harfine göre rafa koydun, sonra adını değiştirdin. Kutu hâlâ eski rafta duruyor, ama sen onu yeni harfin rafında arıyorsun.
Bir nesneyi HashMap'e anahtar olarak koyduktan sonra, o nesnenin hashCode'a giren bir alanını değiştirirseniz ne olur?
Kendin gör
Simülatör her adımda bir anahtar ekliyor. Önce varsayılan (iyi hashCode) ile izle, sonra kaliteyi düşür.
HashMap — bucket, çarpışma, resize ve treeify
Tohum 9- 0—
- 1—
- 2—
- 3—
- 4—
- 5—
- 6—
- 7—
- Dolu bucket
- 0/8
- Çarpışma
- 0
- Resize
- 0
Şu an ne oldu?
0 girdi, 0/8 bucket dolu
En uzun zincir 0. Bucket indeksi hash & (kapasite - 1) ile hesaplanıyor; kapasitenin ikinin kuvveti olmasının sebebi bu maskeleme işlemi.
Aklında kalsın: hashCode bucket’ı seçer, equals bucket içinde doğru girdiyi bulur. İkisini birlikte override etmek zorunlu olmasının sebebi budur.
Olay günlüğü (0)
Henüz olay yok. Oynat veya adımla.
Berbat — return 42. Bütün anahtarlar tek bucket’a düşüyor.Rastgele anahtar aradüğmesine bas veequalsçağrı sayısına bak — arama artık O(n).- Kapasiteyi 64 yap, hashCode’u berbat bırak. Zincir 8’e ulaşınca bucket ağaca dönüşüyor. Aynı testi kapasite 8 ile yap: bu kez treeify yerine resize oluyor.
- Yük faktörünü %100 yap. Resize sayısı düşüyor ama zincirler uzuyor — yük faktörü tam olarak bu takası ayarlar.
Bir anahtarı mutasyona uğrat. Girdi kırmızıya dönüyor ve üstü çiziliyor: az önce satır satır izlediğin hatanın görsel hâli.
Her anahtar adayını HashMap'te güvenli olup olmadığına göre ayır.
Kutuların içinde ne var?
HashMap aslında numaralı kutulardan oluşan bir dolaptır. Kutular doldukça iki şey olur: dolap büyütülür ya da kalabalık bir kutu kendi içini düzenler.
Dolap büyüyünce. Girdiler kutu sayısının dörtte üçünü geçince (yük faktörü 0.75) kutu sayısı ikiye katlanır ve her girdi yeni yerine taşınır. Bu taşıma pahalıdır; kaç eleman koyacağını biliyorsan dolabı baştan büyük aç:
// 1000 eleman koyacaksan: 1000 / 0.75 = 1334 → bir üst ikinin kuvvetivar map = new HashMap<String, User>(2048);Kalabalık kutu. Bir kutuda 8 girdi birikirse Java onları daha hızlı aranan bir ağaca çevirir. Buna treeify denir.
Kutu numarası nasıl hesaplanır?· istersen atla
Kutu sayısı her zaman ikinin kuvvetidir, böylece bölme yerine tek bir bit maskesi yeter:
// capacity = 16 → 16 - 1 = 1111 (binary) → hash'in son 4 bitiint index = hash & (capacity - 1);Bu maske yalnızca düşük bitlere bakar. Java bunu, yüksek bitleri de işe katan bir “spread” adımıyla telafi eder:
static int hash(Object key) { int h = key.hashCode(); return h ^ (h >>> 16); // yüksek 16 biti düşük 16 bitin üstüne XOR'la}Treeify'ın iki sürprizi· istersen atla
| Detay | Sonuç |
|---|---|
| Kapasite 64’ten küçükse treeify olmaz, resize olur | Küçük tabloda uzun zincirin sebebi kötü hashCode değil, küçük tablodur |
Anahtar Comparable değilse | Ağaç sıralaması hashCode ve nesne kimliğine düşer; kazanç azalır |
Treeify aslında bir güvenlik önlemidir: kötü niyetli biri aynı hashCode’u üreten anahtarlar göndererek aramaları bilerek yavaşlatabilir.
Kafam karıştı, daha basit anlat
Kutular dolunca dolap büyür ve herkes yeni yerine taşınır. Tek bir kutu çok kalabalıklaşırsa, o kutu içindekileri daha kolay aranacak şekilde dizer.
HashMap'in kapasitesi neden her zaman ikinin kuvvetidir?
Tuzaklar
== ile equals farkı.
Integer a = 127, b = 127;a == b; // true — Integer cache -128..127 arasını paylaşır
Integer c = 128, d = 128;c == d; // false — cache dışında, farklı nesnelerc.equals(d); // true — değer karşılaştırması== referans karşılaştırır, equals değer. Integer cache’i bu farkı 127’ye kadar gizler, sonra açığa çıkarır.
Aşağıdaki program bir bankanın üç HashMap’ine üç farklı anahtar koyuyor: doğru yazılmış bir IBAN record’u, hashCode’u unutulmuş bir döviz çifti ve eklendikten sonra değişen bir ücret anahtarı.
Derinleş · Bankada üç anahtar, üç HashMap davranışı 4 dosya · ~74 satır · ilk okumada atlayabilirsin
Kendini sına
Önce hızlı bir ısınma: puan yok, kayıt yok.
equals'ı override ettiysen hashCode'u da override etmelisin.
Bu program ne yazdırır?
Aklında kalacak üç şey
- 1 hashCode hangi kutuya bakılacağını, equals o kutudaki doğru girdiyi seçer. Bu yüzden ikisi hep birlikte yazılır.
- 2 HashMap genelde tek adımda bulur, ama hashCode kötü dağılırsa bir listeyi baştan sona yürür. Java 8'den beri uzun zincirler ağaca dönüşüp işi hızlandırır.
- 3 Map'e koyduktan sonra anahtarın hashCode'a giren alanı değişirse girdi bulunamaz. Ama size() onu saymaya devam eder.
5 kart sonraki derste seni bekliyor
Bu dersin üstüne kurulanlar
Bunlar bu dersi temel alıyor; hazır olduğunda devam edebilirsin.
- Core Java & EşzamanlılıkConcurrentHashMap — Güvenli Map, Güvensiz SayaçGüvenli olduğu söylenen bir map'te sayaç neden yine artış kaybeder?Derse git
- Hibernate ve JPAEntity equals ve hashCode — Aynı Sipariş mi?Kaydettiğin nesne bir kümenin içinde kayboldu. equals ve hashCode'u id'ye göre yazmıştın, değil mi?Derse git