İçeriğe geç

HashMap, equals ve hashCode

Başlangıç 10 dk Çok sık karşılaşılır

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.

  1. Bayt: map.put(ali, veri) yaptım. Sonra map.get(ali) dedim ve... null?!

  2. Sen: Ama aynı anahtar! equals bile true diyor.

  3. Bayt: HashMap önce kapı numarasına bakar, sonra kapıdaki isme. Sen hangisini unuttun?

  4. 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) ise a.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.

Hızlı kontrolBaşlangıç

`map.get(anahtar)` çağrısı doğru girdiyi hangi sırayla bulur?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

`equals`'ı override edip `hashCode`'u override etmezseniz HashMap'te ne olur?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

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.

Kapı numarası ile kapıdaki isim: biri bozulursa girdi kaybolur.
Adım adım oku
  1. hashCode, anahtarı bir kapı numarasına (bucket) çevirir: ali için 1.
  2. equals, o kapının arkasındaki girdinin gerçekten aranan anahtar olup olmadığına bakar.
  3. Anahtarın adı veli olunca hashCode da değişir ve yeni numara 3 olur.
  4. 3 numaralı kapı boştur, get null döner. Girdi hâlâ 1 numarada duruyor ve size onu saymaya devam ediyor.

Kaybolan girdi

MutableKeyBug.java
şu an çalışan satırvar key = new User("ali");
2var map = new HashMap<User, String>();
3map.put(key, "veri");
4
5System.out.println(map.get(key));
6
7key.setName("veli");
8
9System.out.println(map.get(key));
10System.out.println(map.size());

Debug

Adım 1/6

main User'ın hashCode'u ad alanından türüyor. Şu an "ali".

key.name
= ali
key.hashCode()
= 96865
Java 21UTF-8LF1:1

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.

Doğru anahtar — record
// equals ve hashCode otomatik, alanlar final
public 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.

Hızlı kontrolBaşlangıç

Bir nesneyi HashMap'e anahtar olarak koyduktan sonra, o nesnenin hashCode'a giren bir alanını değiştirirseniz ne olur?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

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
size 0capacity 8eşik 6en uzun zincir 0
  • 0—
  • 1—
  • 2—
  • 3—
  • 4—
  • 5—
  • 6—
  • 7—
Dolu bucket
0/8
Çarpışma
0
Resize
0
Hız
Adım 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.

  1. Berbat — return 42. Bütün anahtarlar tek bucket’a düşüyor. Rastgele anahtar ara düğmesine bas ve equals çağrı sayısına bak — arama artık O(n).
  2. 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.
  3. Yük faktörünü %100 yap. Resize sayısı düşüyor ama zincirler uzuyor — yük faktörü tam olarak bu takası ayarlar.
  4. 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.
Hızlı kontrolBaşlangıç

Her anahtar adayını HashMap'te güvenli olup olmadığına göre ayır.

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Sınıflandırılmamış

Güvenli anahtar

hashCode'u haritada durduğu sürece değişmez

    Riskli anahtar

    hashCode'u sonradan değişebilir, girdi kaybolur

      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ç:

      Gereksiz resize'lardan kaçınmak
      // 1000 eleman koyacaksan: 1000 / 0.75 = 1334 → bir üst ikinin kuvveti
      var 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:

      HashMap.java — özü
      // capacity = 16 → 16 - 1 = 1111 (binary) → hash'in son 4 biti
      int 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:

      HashMap.hash()
      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
      DetaySonuç
      Kapasite 64’ten küçükse treeify olmaz, resize olurKüçük tabloda uzun zincirin sebebi kötü hashCode değil, küçük tablodur
      Anahtar Comparable değilseAğ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.

      Hızlı kontrolOrta

      HashMap'in kapasitesi neden her zaman ikinin kuvvetidir?

      Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

      Tuzaklar

      == ile equals farkı.

      Sık sorulan tuzak
      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ı nesneler
      c.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
      Proje dosyaları

      src/main/java/bank/ Iban.java Doğru anahtar: IBAN record'u equals ve hashCode'u bileşenden üretir ve değişmez. Boşluklu yazılmış IBAN da aynı anahtara düşer.

      src/main/java/bank/Iban.java
      // equals, hashCode and toString generated from the component; the value cannot change.
      public record Iban(String value) {
      // Normalise once, in the constructor: "TR33 0006 1005 …" and "TR330006100…"
      // become the same value, so they are the same map key.
      public Iban {
      value = value.replace(" ", "").toUpperCase(Locale.ROOT);
      if (!value.matches("[A-Z]{2}\\d{2}[A-Z0-9]{11,30}")) {
      throw new IllegalArgumentException("not an IBAN: " + value);
      }
      }
      }

      src/main/java/bank/ CurrencyPair.java Yarım anahtar: döviz çifti equals yazmış, hashCode'u unutmuş. Eşit iki çift farklı bucket'lara düşer ve kur 'bulunamaz'.

      src/main/java/bank/CurrencyPair.java
      // BUG: equals without hashCode. Two equal pairs get Object's identity hash.
      public final class CurrencyPair {
      private final String base;
      private final String quote;
      public CurrencyPair(String base, String quote) {
      this.base = base;
      this.quote = quote;
      }
      @Override
      public boolean equals(Object o) {
      return o instanceof CurrencyPair other && base.equals(other.base) && quote.equals(other.quote);
      }
      // hashCode() missing: the contract "equal objects have equal hash codes" is broken.
      // Fix: make it a record, or add `return Objects.hash(base, quote);`
      }

      src/main/java/bank/ FeeKey.java Değişen anahtar: müşteri segmenti eklendikten sonra değişiyor; map'teki ücret satırı kayboluyor.

      src/main/java/bank/FeeKey.java
      // BUG: a key whose hash depends on a field that can change.
      public final class FeeKey {
      private String segment; // "STANDARD", "PREMIUM", …
      public FeeKey(String segment) {
      this.segment = segment;
      }
      // Called when the customer is upgraded: convenient, and the source of the bug.
      public void upgradeTo(String segment) {
      this.segment = segment;
      }
      @Override
      public boolean equals(Object o) {
      return o instanceof FeeKey other && segment.equals(other.segment);
      }
      @Override
      public int hashCode() {
      return segment.hashCode();
      }
      }

      src/main/java/bank/ Main.java Hepsini çalıştıran program; çıktı yorumlarda.

      src/main/java/bank/Main.java
      public class Main {
      public static void main(String[] args) {
      Map<Iban, String> owners = new HashMap<>();
      owners.put(new Iban("TR33 0006 1005 1978 6457 8413 26"), "Ayşe Yılmaz");
      System.out.println(owners.get(new Iban("TR330006100519786457841326"))); // Ayşe Yılmaz
      Map<CurrencyPair, BigDecimal> rates = new HashMap<>();
      rates.put(new CurrencyPair("EUR", "TRY"), new BigDecimal("38.4200"));
      System.out.println(new CurrencyPair("EUR", "TRY").equals(new CurrencyPair("EUR", "TRY"))); // true
      System.out.println(rates.get(new CurrencyPair("EUR", "TRY"))); // null — searched the wrong bucket
      Map<FeeKey, BigDecimal> transferFees = new HashMap<>();
      FeeKey key = new FeeKey("STANDARD");
      transferFees.put(key, new BigDecimal("6.50"));
      key.upgradeTo("PREMIUM"); // hash changes while inside the map
      System.out.println(transferFees.get(key)); // null — same object, lost
      System.out.println(transferFees.size()); // 1 — still there, unreachable
      }
      }

      Kendini sına

      Önce hızlı bir ısınma: puan yok, kayıt yok.

      Şimşek turu1/5

      equals'ı override ettiysen hashCode'u da override etmelisin.

      Soru 1/4Başlangıç

      Bu program ne yazdırır?

      Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.
      IntegerCache.java
      1Integer a = 127;
      2Integer b = 127;
      3System.out.println(a == b);
      4
      5Integer c = 128;
      6Integer d = 128;
      7System.out.println(c == d);
      8System.out.println(c.equals(d));
      Java 21UTF-8LF

      Aklında kalacak üç şey

      1. 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. 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. 3 Map'e koyduktan sonra anahtarın hashCode'a giren alanı değişirse girdi bulunamaz. Ama size() onu saymaya devam eder.
      Sonraki kapı Bir listenin başına tek bir eleman eklemek neden bazen herkesi yerinden kaldırır? Koleksiyonların İçi — ArrayList, LinkedList ve Iterator · 8 dk

      5 kart sonraki derste seni bekliyor

      0/5 kart bu dersten toplandı

      Bu dersin üstüne kurulanlar

      Bunlar bu dersi temel alıyor; hazır olduğunda devam edebilirsin.