İçeriğe geç
academia.sh

Ders 03 / 15

Sözlükler

Anahtar-değer sözlüğünün üç sözü — anahtarın tekilliği, anahtar görünümünün canlılığı, yok anahtarın boş değer vermesi — üç gerçekleştirimde de aynı çıkıyor ve arayüze ait oluyor. Gezinme sırası, boş anahtar kabulü ve anahtarın karşılaştırılabilir olma zorunluluğu ise karma ile sıralı gerçekleştirim arasında ayrışıyor. Çağıranın kuralı ilk kez merkeze alınıyor: equals yazıp hashCode yazmayan bir anahtarla arama hiçbir istisna vermeden boş değer döndürüyor, aynı anahtar sıralı bir sözlükte ise sessizce bulunuyor çünkü orada kullanılan söz compareTo.

İçindekiler

Önceki ders liste, küme ve kuyruğun ögeye üç ayrı yoldan eriştiğini ölçtü: konum, üyelik, uç. Üçü de kabın kendi kendine yettiği erişimlerdi — ögeyi bulmak için kabın dışından hiçbir bilgi gerekmiyordu. Sözlük bunu bozuyor. Bir sözlükte ögeye anahtarıyla erişilir ve anahtar çağıranın yazdığı, kütüphanenin hiç görmediği bir sınıf olabilir. Kap yalnız anahtarı sorar; anahtarın kendi kendiyle tutarlı davranıp davranmadığını denetlemez. Bu ders o tutarsızlığın nereye düştüğünü ölçüyor: hangi söz kütüphaneden geliyor, hangi söz çağıranın yazdığı sınıfın omuzlarında duruyor, ve ikisi arasında bir çatlak açıldığında program bunu nasıl haber veriyor — ya da vermiyor.

Önceki iki dersin ölçtüğü her kusur derlenen bir programda anında görünürdü ya da hiç görünmezdi — istisna fırlıyorsa fırlıyordu, fırlamıyorsa davranış tutarlıydı. Sözlükteki anahtar kusuru üçüncü bir yol açıyor: program derlenir, çalışır, hiçbir istisna fırlatmaz, ve yine de yanlış çalışır. Bu üçüncü yol kütüphanenin bir eksikliği değil, kütüphanenin çağırana bıraktığı bir yükümlülüğün karşılıksız kalmasıdır.

Anahtar-Değer Tutmanın Arayüz Sözü

Sözlük de önceki iki dersteki gibi üç gerçekleştirimle (HashMap, LinkedHashMap, TreeMap) sınanır; ölçüm motoru aynıdır, yalnız bu derste kullanılan sorular yenidir.

  • KO11 — Motor önceki iki dersle anlamca birebir; yalnız bu derste sorulan sorular eklenmiştir. Garanti sınıfına yeni bir soru eklemek var olan soruların davranışını değiştirmez.
  • KO12 — Sözlük beş anahtarla, aynı sırayla, aynı doldur yardımcısıyla doldurulur.
// Garanti.java — bir davranisin garantisini kim veriyor: arayuz mu, gerceklestirim mi
import java.util.*;
import java.util.function.*;

public class Garanti {
    static int arayuz = 0;
    static int gerceklestirim = 0;

    final List<String> adlar = new ArrayList<>();
    final List<Supplier<Object>> uretecler = new ArrayList<>();

    Garanti ekle(String ad, Supplier<Object> uretec) {
        adlar.add(ad);
        uretecler.add(uretec);
        return this;
    }

    void baslik(String aile) {
        StringBuilder sb = new StringBuilder(String.format("%-32s", aile));
        for (String ad : adlar) sb.append(String.format("%-15s", ad));
        sb.append("garantiyi veren");
        System.out.println(sb);
    }

    void sor(String soru, Function<Object, String> gozlem) {
        List<String> yanitlar = new ArrayList<>();
        for (Supplier<Object> u : uretecler) {
            String y;
            try {
                y = gozlem.apply(u.get());
            } catch (RuntimeException e) {
                y = "hata";
            }
            yanitlar.add(y);
        }
        boolean ortak = new HashSet<>(yanitlar).size() == 1;
        if (ortak) arayuz++; else gerceklestirim++;
        StringBuilder sb = new StringBuilder(String.format("%-32s", soru));
        for (String y : yanitlar) sb.append(String.format("%-15s", y));
        sb.append(ortak ? "arayuz" : "gerceklestirim");
        System.out.println(sb);
    }

    static void ozet() {
        System.out.printf("gozlem: %d arayuz garantisi, %d gerceklestirim davranisi%n",
                arayuz, gerceklestirim);
    }
}
// Sozluk.java — anahtar-deger tutmanin arayuz sozu ile karma/sirali ayrimi
import java.util.*;

public class Sozluk {
    static final List<String> KAYNAK = List.of("zulu", "delta", "alfa", "carli", "bravo");

    static Map<String, Integer> doldur(Map<String, Integer> m) {
        for (int i = 0; i < KAYNAK.size(); i++) m.put(KAYNAK.get(i), i);
        return m;
    }

    @SuppressWarnings("unchecked")
    public static void main(String[] args) {
        Garanti sozluk = new Garanti()
                .ekle("HashMap", () -> doldur(new HashMap<>()))
                .ekle("LinkedHashMap", () -> doldur(new LinkedHashMap<>()))
                .ekle("TreeMap", () -> doldur(new TreeMap<>()));
        sozluk.baslik("sozluk");
        sozluk.sor("anahtar tekil mi", o -> {
            Map<String, Integer> m = (Map<String, Integer>) o;
            m.put("alfa", 99);
            return String.valueOf(m.size());
        });
        sozluk.sor("anahtar gorunumu canli mi", o -> {
            Map<String, Integer> m = (Map<String, Integer>) o;
            Set<String> g = m.keySet();
            m.put("ekstra", 5);
            return String.valueOf(g.contains("ekstra"));
        });
        sozluk.sor("yok anahtarin yaniti ne", o -> String.valueOf(((Map<String, Integer>) o).get("yok")));
        sozluk.sor("gezinme sirasi ekleme sirasi mi",
                o -> String.valueOf(new ArrayList<>(((Map<String, Integer>) o).keySet())
                        .equals(KAYNAK)));
        sozluk.sor("bos anahtar kabul ediliyor mu", o -> {
            try { ((Map<String, Integer>) o).put(null, 0); return "evet"; }
            catch (RuntimeException e) { return "hayir"; }
        });
        System.out.println();
        Garanti.ozet();
    }
}
sozluk                          HashMap        LinkedHashMap  TreeMap        garantiyi veren
anahtar tekil mi                5              5              5              arayuz
anahtar gorunumu canli mi       true           true           true           arayuz
yok anahtarin yaniti ne         null           null           null           arayuz
gezinme sirasi ekleme sirasi mi false          true           false          gerceklestirim
bos anahtar kabul ediliyor mu   evet           evet           hayir          gerceklestirim

gozlem: 3 arayuz garantisi, 2 gerceklestirim davranisi

Üç satır arayüz sütununda topluyor. Bir anahtara ikinci kez yazmak boyutu büyütmüyor — üçü de beş anahtarlı kalıyor, çünkü Map arayüzü anahtarın tekil olacağını doğrudan tanımlıyor. keySet()’in döndürdüğü küme kabın kendisine bağlı bir görünümdür: sözlüğe sonradan eklenen "ekstra" anahtarı, daha önce alınmış görünümde de beliriyor; görünüm bir anlık kopya değil, kabın kendisine açılan bir penceredir. Üçüncü satır ise sözlüğün en sık unutulan sözüdür: olmayan bir anahtarı aramak istisna fırlatmıyor, boş değer döndürüyor. Üç ayrı gerçekleştirim de bu üç sözü aynen tutuyor; bir sözlük seçerken bu üçü tartışmasız kabul edilebilir.

Bu üçüncü sözün kendi sınırı var ve onu görmek için tek bir satır yeterli. Sözlük boş değeri değer olarak da kabul ediyorsa, get’in boş dönmesi iki ayrı durumu ayırt edemez: anahtar hiç yok, ya da anahtar var ve karşılığı zaten boş değer.

// Belirsizlik.java — get(anahtar) yokluk ile bos degeri ayirt edemiyor
import java.util.*;

public class Belirsizlik {
    public static void main(String[] args) {
        Map<String, Integer> m = new HashMap<>();
        m.put("alfa", null);
        System.out.println("get(alfa)         : " + m.get("alfa"));
        System.out.println("get(yok)          : " + m.get("yok"));
        System.out.println("containsKey(alfa) : " + m.containsKey("alfa"));
        System.out.println("containsKey(yok)  : " + m.containsKey("yok"));
    }
}
get(alfa)         : null
get(yok)          : null
containsKey(alfa) : true
containsKey(yok)  : false

İki get çağrısı da aynı yanıtı, boş değeri, veriyor; ama biri gerçekten yok olan bir anahtarı, öteki var olan ve kasıtlı olarak boş değere bağlanmış bir anahtarı anlatıyor. get’in tek başına yeterli olmadığı yer burası: yokluk sözünü tam okumak için containsKey ayrı bir çağrı olarak gerekiyor. Arayüzün sözü “yok anahtar boş değer döndürür”dür; “boş değer dönerse anahtar yoktur” değildir — ikisi aynı önerme değil.

Karma ile Sıralının Ayrıldığı Yer

Tablonun alt iki satırı HashMap/LinkedHashMap ile TreeMap’i ayırıyor. Gezinme sırası HashMap’te anahtarların karma değerine, LinkedHashMap’te ekleme sırasına, TreeMap’te anahtarların doğal sırasına bakıyor — üçü de arayüzün değil, seçilen sınıfın kararı. Boş anahtar kabulü de aynı çizgide ayrışıyor: HashMap ve LinkedHashMap boş anahtarı tek bir özel yuvada tutuyor, TreeMap reddediyor, çünkü bir anahtarı yerleştirmek için onu var olan anahtarlarla karşılaştırması gerekiyor ve boş değerin karşılaştırılacak bir sırası yok.

Bu son gözlem üçüncü bir ayrımı işaret ediyor: TreeMap’in anahtardan istediği şey HashMap’in istediğinden farklı. HashMap bir anahtarı yalnız equals ve hashCode ile tanır; TreeMap bir sıra bekler. Bu, arayüzün kendisinde görünmeyen bir farktır — ikisi de Map<K, V> gerçekleştirir, ikisinin de imzasında K üzerine hiçbir ek kısıt yazmaz. Fark yalnız çalışma zamanında, anahtar ilk kez yerleştirilmeye çalışıldığında ortaya çıkar. Anahtar sınıfı sıralanabilir değilse ne olur?

  • KO13 — Aynı anahtar sınıfı equals ve hashCode’u eksiksiz yazar ama hiçbir sıralama bildirmez (Comparable gerçekleştirmez). Üç sözlüğe de aynı tek anahtarla tek bir put çağrısı yapılır.
// Zorunluluk.java — anahtarin siralanabilir olmasi hangi gerceklestirimde zorunlu
import java.util.*;

public class Zorunluluk {
    static class KarsilastirilamazAnahtar {
        final String ad;
        KarsilastirilamazAnahtar(String ad) { this.ad = ad; }
        @Override public boolean equals(Object o) {
            return o instanceof KarsilastirilamazAnahtar a && a.ad.equals(ad);
        }
        @Override public int hashCode() { return ad.hashCode(); }
    }

    static String dene(Runnable islem) {
        try { islem.run(); return "basarili"; }
        catch (RuntimeException e) { return e.getClass().getSimpleName(); }
    }

    public static void main(String[] args) {
        Map<KarsilastirilamazAnahtar, Integer> hash = new HashMap<>();
        Map<KarsilastirilamazAnahtar, Integer> baglantili = new LinkedHashMap<>();
        Map<KarsilastirilamazAnahtar, Integer> siralanan = new TreeMap<>();

        System.out.println("HashMap.put       : " + dene(() -> hash.put(new KarsilastirilamazAnahtar("a"), 1)));
        System.out.println("LinkedHashMap.put : " + dene(() -> baglantili.put(new KarsilastirilamazAnahtar("a"), 1)));
        System.out.println("TreeMap.put       : " + dene(() -> siralanan.put(new KarsilastirilamazAnahtar("a"), 1)));
    }
}
HashMap.put       : basarili
LinkedHashMap.put : basarili
TreeMap.put       : ClassCastException

equals ve hashCode tam yazılmış, hiçbir eksik yok — ve yine de üçüncü satır çöküyor. HashMap ile LinkedHashMap anahtarı yuvasına yerleştirmek için yalnız karma değerini ve eşitliğini bilmesi gerekiyor; TreeMap ise ilk put çağrısında bile anahtarı ağaçtaki konumuna yerleştirmek için onu bir şeyle karşılaştırmak zorunda ve elinde ne bir Comparable ne bir Comparator var. Kusur derleme zamanında görünmüyor — Map<K, V> tanımı K üzerine hiçbir sıralama kısıtı koymuyor — ve çalışma zamanında, ilk yazmada, adıyla düşüyor.

Çağıranın Kuralı: equals Yazıp hashCode Yazmamak

Yukarıdaki iki bölüm arayüzün ve gerçekleştirimin sözlerini ayırdı. Geriye üçüncü kaynak kalıyor: çağıranın kendi yazdığı anahtar sınıfının uyması gereken kural. Object sınıfı bir sözleşme tanımlar — iki nesne equals ile eşit sayılıyorsa hashCode’ları da eşit olmalıdır — ama derleyici bu sözleşmeyi hiçbir yerde denetlemez. equals yazılıp hashCode yazılmadığında ne olur?

  • KO14 — Anahtar sınıfı equals’ı doğru yazar (adı karşılaştırır) ve hashCode’u hiç yeniden tanımlamaz; miras aldığı Object.hashCode() çalışma zamanı kimliğine bakar. İki ayrı new çağrısı aynı adı taşısa da iki ayrı karma değeri üretir.
// EslesmeyenAnahtar.java — equals yazilmis, hashCode yazilmamis bir anahtarla arama
import java.util.*;

public class EslesmeyenAnahtar {
    static class Anahtar {
        final String ad;
        Anahtar(String ad) { this.ad = ad; }
        @Override public boolean equals(Object o) {
            return o instanceof Anahtar a && a.ad.equals(ad);
        }
        // hashCode kasitli olarak yazilmadi
    }

    public static void main(String[] args) {
        Map<Anahtar, Integer> depo = new HashMap<>();
        depo.put(new Anahtar("alfa"), 9);
        Integer bulunan = depo.get(new Anahtar("alfa"));
        String sonuc = String.valueOf(bulunan);
        System.out.printf("%-30s %-9s bulunan=%-6s beklenen=9%n", "hashCode yazilmamis anahtar",
                sonuc.equals("9") ? "basarili" : "sessiz", sonuc);
    }
}
hashCode yazilmamis anahtar    sessiz    bulunan=null   beklenen=9

Yazan da arayan da aynı adı, "alfa"’yı, kullanıyor; equals ikisini eşit sayardı. Ama HashMap anahtarı önce hangi yuvaya bakacağına karar vermek için hashCode’a başvuruyor ve iki ayrı new Anahtar("alfa") nesnesi, Object’ten miras kalan varsayılan hashCode yüzünden, neredeyse kesinlikle iki ayrı yuvaya düşüyor. Arama, değerin durduğu yuvaya hiç uğramıyor ve equals çağrılma fırsatı bile bulamıyor. Sonuç sessiz: hiçbir istisna fırlamıyor, get yalnızca boş değer döndürüyor — sanki anahtar hiç eklenmemiş gibi. Kusurun görüneceği yer yazma satırı değil, aylar sonra yazılmış bir arama satırı olabilir.

Bu üç sınıfa ayrılan sonuç — istisna, sessiz, etkisiz — kursun kendi sınıflamasıdır ve burada ilk kez somut bir örnekle karşılanıyor. hashCode eksikliği istisna sınıfına girmiyor: hiçbir satır çökmüyor, program baştan sona çalışıyor. Aynı sözlük binlerce anahtarla dolu olsaydı, kusur yine görünmezdi; yalnızca yanlış bir sayıda öge “kayıp” görünürdü ve bu kaybın nedeni günler sonra, kaynağa hiç dokunmadan bulunması gereken bir hata olurdu.

Sınırlayıcı Ölçüm: Sıralı Sözlükte Aynı Anahtar Bulunuyor

Bir önceki ölçüm “bu anahtar sınıfı bozuk” sonucuna götürüyor gibi görünüyor. Sınırlayıcı ölçüm bunu düzeltiyor: aynı sınıf, aynı eksik hashCode, tek fark gerçekleştirim seçimi.

  • KO15 — Anahtar sınıfı yukarıdakiyle birebir aynıdır; hashCode yine yazılmamıştır. Tek değişen, sözlüğün TreeMap olması ve bir Comparator ile kurulmasıdır.
// SiraliDeBulunuyor.java — ayni kusurlu anahtar siralamaya dayanan sozlukte bulunuyor
import java.util.*;

public class SiraliDeBulunuyor {
    static class Anahtar {
        final String ad;
        Anahtar(String ad) { this.ad = ad; }
        @Override public boolean equals(Object o) {
            return o instanceof Anahtar a && a.ad.equals(ad);
        }
        // hashCode kasitli olarak yazilmadi
    }

    public static void main(String[] args) {
        Map<Anahtar, Integer> siralanan = new TreeMap<>(Comparator.comparing(a -> a.ad));
        siralanan.put(new Anahtar("alfa"), 9);
        Integer bulunan = siralanan.get(new Anahtar("alfa"));
        System.out.println("TreeMap.get, ayni kusurlu anahtar : " + bulunan);
    }
}
TreeMap.get, ayni kusurlu anahtar : 9

Aynı sınıf, aynı eksik yöntem, ve arama bu kez buluyor. Fark anahtarda değil, anahtarı okuyan gerçekleştirimde: TreeMap bir konum bulmak için hiçbir zaman hashCode’a bakmıyor, yalnız verilen Comparator’ı çağırıyor ve o da yalnız ad alanını okuyor. Bu, ilk ölçümün yanlış okunmasını engelliyor. Kusurlu olan anahtar sınıfının kendisi değildi; kusur, bir sınıfın verdiği sözle (yalnız equals) seçilen gerçekleştirimin beklediği sözün (hashCode ve equals birlikte) eşleşmemesiydi. Aynı anahtar, beklentisi farklı bir gerçekleştirimin elinde, kusursuz çalışıyor.

Bu iki ölçüm yan yana konduğunda kursun sorusu tam olarak buraya geliyor. Bir kusuru tarif ederken “bu sınıf bozuk” demek çoğu zaman yanlış sorudur; doğru soru “bu sınıf hangi sözü veriyor ve onu okuyan taraf hangi sözü bekliyor” sorusudur. HashMap hashCode bekler ve anahtar onu vermez — kusur orada. TreeMap yalnız bir sıra bekler ve anahtar, dolaylı yoldan bir Comparator üzerinden, onu verir — kusur orada yok. Anahtar sınıfı hiç değişmedi; değişen, anahtarın hangi sözleşmeye karşı sınandığıydı.

Özet

  • Sözlüğün üç sözü — anahtarın tekilliği, anahtar görünümünün canlılığı, yok anahtarın boş değer vermesi — üç gerçekleştirimde de aynı çıktı ve arayüze aitti.
  • Gezinme sırası ve boş anahtar kabulü karma ile sıralı gerçekleştirim arasında ayrıştı; ayrımın kökeninde TreeMap’in anahtarı karşılaştırma zorunluluğu var.
  • Sıralanamayan bir anahtar HashMap’te sorunsuz çalışırken TreeMap’te ilk yazmada ClassCastException ile düşüyor — kusur derlemede değil, çalışma zamanında görünüyor.
  • Çağıranın kuralı: equals yazılıp hashCode yazılmadığında arama hiçbir istisna vermeden boş değer döndürüyor; kusur yazma satırında değil, arama satırında ortaya çıkıyor.
  • Sınırlayıcı ölçüm: aynı eksik anahtar sınıfı sıralı bir sözlükte sorunsuz bulunuyor, çünkü orada aranan söz compareTo/Comparator’dır. Kusur anahtarda değil, anahtarla gerçekleştirimin beklentisinin eşleşmemesindeydi.

Sonraki Adım

Bu derste bir sözlüğün anahtarı bulup bulamayacağı ölçüldü; peki kap gezinirken değişirse ne olur? Bir sözlüğün ya da kümenin üzerinde for ile ilerlerken aynı kaba yeni bir öge eklemek ya da bir ögeyi silmek, üç ailenin de karşılaştığı ortak bir durumdur. Sıradaki ders bu durumu ölçer: bazı gerçekleştirimler hemen ve adıyla bir istisna fırlatır, bazıları hiçbir şey söylemeden yanlış bir gezinti tamamlar — ve hangisinin olacağına yine arayüz değil, gerçekleştirim karar verir. Bu derste görülen “kusur derlenir, çalışır, ve sessizce yanlış sonuç verir” örüntüsü orada bir kez daha, farklı bir sebeple karşımıza çıkacak.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat