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.
Garantisı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ı
dolduryardı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ı
equalsvehashCode’u eksiksiz yazar ama hiçbir sıralama bildirmez (Comparablegerçekleştirmez). Üç sözlüğe de aynı tek anahtarla tek birputç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) vehashCode’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;
hashCodeyine yazılmamıştır. Tek değişen, sözlüğünTreeMapolması ve birComparatorile 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ışırkenTreeMap’te ilk yazmadaClassCastExceptionile düşüyor — kusur derlemede değil, çalışma zamanında görünüyor. - Çağıranın kuralı:
equalsyazılıphashCodeyazı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.