Ders 02 / 15
Liste, Küme ve Kuyruk
Aynı altı ögelik iş yükü liste, küme ve kuyruk gerçekleştirimine birden verilip her birinin girdiden ne düşürdüğü sayılıyor: liste hiçbir şey düşürmüyor, küme üç bilgiyi düşürüyor, kuyruk yalnızca konum erişimini. Sınırlayıcı ölçüm LinkedList'in aynı anda hem liste hem kuyruk olmasından doğuyor: iki arayüz de bir remove sözü veriyor, sözler tek bir çağrı satırında çakışıyor ve birebir aynı yazılan remove(1) iki ayrı iş yapıyor — garantilerin ikisi de yerinde, boşluk çağıranın yazdığı satırda ve kırılması sessiz.
İçindekiler
Önceki ders dört aileyi tek bir bütün olarak ölçtü ve onbeş gözlemin kaçının arayüzden,
kaçının seçilen sınıftan geldiğini saydı. Ama liste, küme ve kuyruk arasındaki seçimin
kendisi hiç sorgulanmadı — üçü de birer koleksiyondur, üçü de aynı add çağrısını kabul
eder. Bu ders o seçimi açar: aynı veri üçüne birden verildiğinde her biri neyi tutuyor,
neyi sessizce bırakıyor? Yanıt, hiçbirinin ötekinden “daha iyi” olmadığını, üçünün de
farklı bir soruya yanıt vermek üzere tasarlandığını gösterecek.
Veri Yapıları kursu liste, yığıt, kuyruk ve kümeyi soyut veri tipi olarak kurdu ve gerçekleştirim seçeneklerini karmaşıklıkla karşılaştırdı; o karşılaştırma burada tekrarlanmıyor. Buradaki soru büyüklük değil sözdür: bir liste seçmekle bir küme seçmek arasındaki fark, işlemlerin hızından önce, hangi bilginin saklanacağının taahhüdüdür.
Üç arayüzün adı bile bir vaat taşır: liste sıralı bir dizin çağrıştırır, küme matematikteki kümenin tekillik özelliğini, kuyruk fiziksel bir sıranın iki ucunu. Bu ders adların çağrıştırdığı sezgiyi ölçülebilir bir sayıya indiriyor: aynı girdi karşısında her arayüz kaç bilgiyi koruyor, kaçını feda ediyor — ve feda edilenin rastlantı mı, arayüzün kendi sözü mü olduğu ayrı ayrı sınanıyor.
Aynı İş Yükü, Üç Söz
- KO7 — İş yükü altı ögelik sabit bir listedir ve içinde iki yinelenen değer vardır
(
elmavearmutikişer kez geçiyor); üç aile de aynı listeden, aynı sırayla doldurulur. - KO8 — Ölçülen üç bilgi birbirinden bağımsızdır: yinelenen ögenin tutulup tutulmadığı, ekleme sırasının korunup korunmadığı, konuma (indekse) doğrudan erişilip erişilmediği. Bir ailenin kaç bilgiyi düşürdüğü bu üçünün toplamıdır.
Her aileden yalnız bir gerçekleştirim seçildi (ArrayList, HashSet, ArrayDeque) —
önceki dersteki gibi üç gerçekleştirimi karşılaştırıp arayüz ile gerçekleştirimi ayırmak bu
derste tekrarlanmıyor, çünkü soru burada başka: aynı gerçekleştirim seçiliyken üç arayüz
birbirinden ne kadar ayrışıyor. HashSet bilinçli seçildi, çünkü onun gezinme sırası
girdiyle örtüşmez; sırayı koruyan bir küme gerçekleştirimi seçilseydi bu bölümdeki ölçüm
gizlenirdi.
// UcSoz.java — ayni is yukunu uc arayuze verip hangi bilginin dustugunu sayar
import java.util.*;
public class UcSoz {
static final List<String> ISYUKU = List.of("elma", "armut", "elma", "kiraz", "armut", "erik");
static void olc(String aile, Collection<String> kap) {
boolean yinelemeTutuluyor = kap.size() == ISYUKU.size();
boolean siraTutuluyor = new ArrayList<>(kap).equals(ISYUKU);
boolean indeksErisimVar = kap instanceof List;
int dusen = (yinelemeTutuluyor ? 0 : 1) + (siraTutuluyor ? 0 : 1) + (indeksErisimVar ? 0 : 1);
System.out.printf("%-8s boyut=%-3d yineleme=%-6s sira=%-6s indeks=%-6s dusen=%d%n",
aile, kap.size(), yinelemeTutuluyor, siraTutuluyor, indeksErisimVar, dusen);
}
public static void main(String[] args) {
olc("liste", new ArrayList<>(ISYUKU));
olc("kume", new HashSet<>(ISYUKU));
olc("kuyruk", new ArrayDeque<>(ISYUKU));
}
}
liste boyut=6 yineleme=true sira=true indeks=true dusen=0 kume boyut=4 yineleme=false sira=false indeks=false dusen=3 kuyruk boyut=6 yineleme=true sira=true indeks=false dusen=1
Üç satır üç ayrı taahhüt anlatıyor. ArrayList girdiden hiçbir şey düşürmüyor: altı
öge de kalıyor, sırası korunuyor, herhangi bir konuma get ile doğrudan gidilebiliyor.
HashSet girdinin üçünü birden düşürüyor: altı ögelik girdi dört ögeye iniyor (iki
yinelenen atıldı), kalan dördün gezinme sırası girdiyle örtüşmüyor, konum erişimi hiç yok.
ArrayDeque ise yalnızca birini düşürüyor: yineleneni de tutuyor, sırayı da koruyor,
düşürdüğü tek şey konum erişimi.
Söz Karmaşıklıktan Değil Sözden Gelir
Kümenin düşürdüğü üç bilgi bir kusur değil, tekilliğin bedelidir: bir kap her ögeyi bir
kez tutacağını vaat ediyorsa, ikinci elma girdiği anda ya reddedilir ya da birinciyle
birleşir — ikisi de aynı sonucu, boyutun küçülmesini, doğurur. Kümenin sözü tektir:
tekillik. Sıranın ne olacağını söylemez, çünkü tekilliği sağlamanın hangi iç yapıyla
yapıldığı (karma tablosu, dengeli ağaç, ekleme sıralı bir bağlı liste) sıraya karışır ve
arayüz bu iç yapıyı seçmez — önceki derste HashSet, LinkedHashSet ve TreeSet’in aynı
soruya üç ayrı yanıt vermesi tam olarak bunun ölçümüydü.
Kümenin düşürdüğü sıra bilgisi, tekilliğin aksine, arayüzün zorunlu kıldığı bir kayıp değildir — bunu doğrudan ölçmek mümkün. Aynı iş yükü iki küme gerçekleştirimine verilip sonucun ilk-görülen sırayla (yinelenenler atıldıktan sonra kalan dört ögenin girdideki ilk geçiş sırası) örtüşüp örtüşmediğine bakılır.
// Tekil.java — kumenin dusurdugu sira arayuzun zorunlu kildigi bir sey mi
import java.util.*;
public class Tekil {
static final List<String> ISYUKU = List.of("elma", "armut", "elma", "kiraz", "armut", "erik");
static final List<String> ILKGORULEN = List.of("elma", "armut", "kiraz", "erik");
static boolean siraIlkGorulenleAyniMi(Collection<String> kap) {
return new ArrayList<>(kap).equals(ILKGORULEN);
}
public static void main(String[] args) {
Set<String> hash = new HashSet<>(ISYUKU);
Set<String> baglantili = new LinkedHashSet<>(ISYUKU);
System.out.println("HashSet gezinme sirasi : " + hash);
System.out.println("HashSet sira ilk-gorulenle ayni mi : " + siraIlkGorulenleAyniMi(hash));
System.out.println("LinkedHashSet gezinme sirasi : " + baglantili);
System.out.println("LinkedHashSet sira ilk-gorulenle ayni mi : " + siraIlkGorulenleAyniMi(baglantili));
}
}
HashSet gezinme sirasi : [erik, elma, armut, kiraz] HashSet sira ilk-gorulenle ayni mi : false LinkedHashSet gezinme sirasi : [elma, armut, kiraz, erik] LinkedHashSet sira ilk-gorulenle ayni mi : true
LinkedHashSet aynı tekillik sözünü tutarken sırayı da koruyor; HashSet korumuyor. Demek
ki kümenin sırayı düşürmesi arayüzün dayattığı bir şey değil, seçilen sınıfın bir tercihidir
— tıpkı önceki derste dört ailede ölçülen “gezinme sirasi ekleme sirasi mi” satırlarının
gerceklestirim diye işaretlenmesi gibi. Arayüzün zorunlu kıldığı tek şey tekilliktir; sıra,
konum erişimi gibi, sınıftan sınıfa değişebilen bir eklentidir.
Kuyruğun sözü de tektir ve kümeninkinden farklıdır: uç. Queue arayüzü offer,
poll, peek üçlüsüyle “bir uçtan eklenir, bir uçtan çıkarılır ve o uç sabittir” der;
hangi ögenin o uçta bekleyeceğini söylemez. ArrayDeque bu iş yükünde ekleme sırasını
tesadüfen değil, kendi gerçekleştirim tercihiyle (uçtan uca bir dizi) koruyor; önceki derste
aynı arayüzü konuşan PriorityQueue’nun aynı soruya false yanıtı vermesi, sıranın
kuyruğun sözü olmadığını zaten gösterdi. Kuyruğun burada kaybettiği tek bilgi konum
erişimidir, çünkü uç sözü veren bir kap ortadaki bir ögeye adres vermez — adres vermek
listenin işidir.
Liste ise iki sözü birden taşır: sıra ve yineleme. Bir listeye eklenen her öge,
eşiti daha önce eklenmiş olsa bile, kendi konumunda kalır; konumların hepsi get ile
adreslenebilir. Bu iki söz kümenin ve kuyruğun tek tek verdiği sözlerin toplamı değildir —
liste tekillik vaat etmez (iki elma da kalır), kuyruk gibi tek bir uçla sınırlı da
değildir (herhangi bir konumdan okunabilir). Üç arayüz üç ayrı ihtiyaca karşılık gelir ve
seçim, hangi bilginin tutulmasının gerektiğine bakılarak yapılır. Bu iki sözü aynı anda
taşımanın bir bedeli de vardır, ama o bedel bu derste ölçülmüyor: ortadaki bir konuma ekleme
ya da bir değerin var olup olmadığını arama, gerçekleştirime göre değişen bir maliyet taşır
ve bu maliyet Veri Yapıları kursunda zaten sayılmıştı. Burada ölçülen yalnızca hangi bilginin
kalıp kalmadığı; ne kadar sürede kaldığı ayrı bir eksendir.
Önceki dersteki isteğe bağlı işlem gözlemi burada bir kez daha karşımıza çıkıyor. Set.of
ile üretilen sabit bir küme de, List.of ile üretilen sabit bir liste de ekleme
girişimini aynı istisnayla reddediyordu; bu ders reddin biçimini değil, reddedilmeyen
durumda hangi bilginin kaldığını ölçüyor. İki ölçüm birbirini tamamlıyor: biri “ekleme
kabul edilirse ne olur” sorusuna, öteki “ekleme reddedilirse ne görürüz” sorusuna yanıt
veriyor. İkisi de aynı sonuca çıkıyor: bir kabın davranışını anlamak için tek bir çağrının
yazımı yetmiyor, hangi arayüzün hangi sözü verdiğini ayrı ayrı bilmek gerekiyor.
Sınırlayıcı Ölçüm: Aynı Nesne, İki Anlam
LinkedList çerçevenin tek çift kimlikli sınıfıdır: hem List hem Queue (Deque
üzerinden) arayüzünü aynı anda gerçekleştirir. Yukarıdaki ölçümde kuyruk satırı
ArrayDeque ile temsil edildi ve liste satırı ArrayList ile; ikisi de tek bir arayüzün
sözünü taşıyan, saf sınıflardı. LinkedList saf değildir — aynı nesne, hangi referanstan
çağrıldığına bağlı olarak, hem listenin hem kuyruğun sözünü taşıyabilir, ve bu iki söz
aynı yöntem adını paylaştığında hangisinin geçerli olacağı çağrı yazımından görünmez.
Nesneye Dayalı Java kursunda ölçülen kural burada aynen geçerlidir: bir çağrının hangi
yönteme bağlanacağını çalışma zamanı tipi değil, bildirilen tip belirler.
- KO9 — Tek bir
LinkedListnesnesi kurulur ve iki değişkene atanır: biriList, ötekiQueuetipiyle bildirilmiştir. İkisi de aynı nesneyi gösterir;==ile doğrulanabilir ama ölçüm buna gerek duymaz, çünkü ikinci çağrının etkisi birincinin bıraktığı durumu değiştiriyor.
// Ikilem.java — ayni nesne, iki arayuz, iki ayri remove yontemi
import java.util.*;
public class Ikilem {
public static void main(String[] args) {
LinkedList<Integer> gercek = new LinkedList<>(List.of(10, 20, 30, 40));
List<Integer> listeRef = gercek;
Queue<Integer> kuyrukRef = gercek;
System.out.println("baslangic : " + gercek);
boolean silindiMi = kuyrukRef.remove(1);
System.out.println("kuyruk referansindan remove(1) -> donen=" + silindiMi
+ " sonuc=" + gercek);
Integer silinenDeger = listeRef.remove(1);
System.out.println("liste referansindan remove(1) -> donen=" + silinenDeger
+ " sonuc=" + gercek);
}
}
baslangic : [10, 20, 30, 40] kuyruk referansindan remove(1) -> donen=false sonuc=[10, 20, 30, 40] liste referansindan remove(1) -> donen=20 sonuc=[10, 30, 40]
Yazılan satır ikisinde de birebir aynıdır: remove(1). Sonuç hiç aynı değildir. Queue
arayüzünde remove(int) diye bir yöntem yoktur; derleyici 1 argümanını Integer’a
kutulayıp Collection’dan gelen remove(Object)’e bağlar — kap içinde değeri 1 olan bir
öge arar, bulamaz, false döndürür ve hiçbir şeyi değiştirmez. List arayüzünde ise
remove(int) doğrudan tanımlıdır ve derleyici 1’i bir indeks olarak okur; ikinci
konumdaki 20 silinir ve silinen değer geri döner. Aynı iki karakter — (1) — bir yerde
“değeri bul ve sil”, ötekinde “ikinci konumu sil” anlamına geliyor.
Burada kırılan hiçbir garanti yoktur. İki arayüz de sözünü tam tutuyor: Queue bir
ögeyi değerine göre silmeyi, List bir ögeyi konumuna göre silmeyi vaat etmişti ve ikisi
de vaat ettiğini yaptı. Gerçekleştirim de suçsuzdur; nesne baştan sona aynı LinkedList
nesnesidir ve içinde hiçbir şey değişmedi. Boşluk çağıranın yazdığı satırdadır: iki
sözün aynı ada ve aynı yazıma düştüğü yerde hangisini istediğini söylemek çağıranın
yükümlülüğüdür.
Bu yükümlülük kırıldığında sonuç, kursun ölçtüğü üç sınıftan sessiz olanıdır. “İkinci
ögeyi sil” niyetiyle yazılan çağrı hiçbir istisna fırlatmaz, hiçbir şey yapmaz ve
false döner; dönüş değeri okunmazsa kusur fark edilmeden geçer. LinkedList’in iki
arayüzü birden konuşması burada bir kolaylık değil, bir tuzaktır — çünkü tek bir sınıf, iki
sözü aynı çağrı yazımında çakıştırabilen ender yerlerden biridir.
- KO10 — Çağıranın söylemesi gereken şey yalnızca hangi arayüzden konuştuğu değil,
argümanı hangi biçimde yazdığıdır. Aynı
Listüzerinden argüman açıkçaIntegerolarak kutulanırsa “değerle sil” sözü seçilir — ölçüm bunu tek bir referansla, ikinci bir değişken açmadan gösteriyor.
// Cozum.java — karari veren referansin tipi degil, argumanin tipi
import java.util.*;
public class Cozum {
public static void main(String[] args) {
LinkedList<Integer> gercek = new LinkedList<>(List.of(10, 20, 30, 40));
List<Integer> listeRef = gercek;
boolean silindiMi = listeRef.remove(Integer.valueOf(1));
System.out.println("liste referansi, remove(Integer.valueOf(1)) -> donen="
+ silindiMi + " sonuc=" + gercek);
Integer silinenDeger = listeRef.remove(1);
System.out.println("liste referansi, remove(1) (int) -> donen="
+ silinenDeger + " sonuc=" + gercek);
}
}
liste referansi, remove(Integer.valueOf(1)) -> donen=false sonuc=[10, 20, 30, 40] liste referansi, remove(1) (int) -> donen=20 sonuc=[10, 30, 40]
Değişken aynı listeRef, konuşulan arayüz aynı List<Integer>. Yalnızca argümanın yazımı
değişti: birinci çağrıda Integer.valueOf(1) bir nesnedir ve “değerle sil” sözüne düşer —
kap içinde 1 değerine eşit bir öge aranır, bulunamaz, false döner. İkinci çağrıda 1
bir int sabitidir ve “konumla sil” sözüne düşer. Demek ki yükümlülük arayüz seçimiyle
bitmiyor: çağıran, argümanı yazarken de hangi sözü istediğini söylemiş oluyor ve bunu
farkında olmadan söyleyebiliyor. Nesneye Dayalı Java kursu bu seçimin hangi tarafta
yapıldığını ölçmüştü; burada ölçülen şey seçimin tarafı değil, seçimi çağırana bırakan
kütüphanenin bunu hiçbir yerde sormamasıdır.
Bu tuzağın yalnız LinkedList üzerinde gösterilebilir olmasının nedeni onun iki arayüzü
birden konuşmasıdır. ArrayList yalnızca List’i, ArrayDeque yalnızca Queue’yu konuşur;
o sınıflarda remove(1) yazımının karşılığı tektir ve çakışma hiç doğmaz. LinkedList ise
iki sözü aynı nesnede toplar; bu yüzden ölçüm iki ayrı sınıf değil, tek bir nesne
üzerinde, yalnız iki değişkenle yapılabiliyor. “Konumla sil” yalnızca List tarafında
vardır, Queue tarafında hiç yoktur — ayrım nesnenin davranışından değil, iki arayüzün
verdiği sözlerin kümesinden doğuyor. Bu, kütüphanede birden çok arayüzü aynı anda
gerçekleştiren her sınıf için geçerli genel bir kuraldır; LinkedList bunu tek bir çağrıyla
göstermeye en yakın örnektir.
Özet
- Aynı altı ögelik girdi listeye, kümeye ve kuyruğa verildiğinde liste hiçbir bilgi düşürmüyor, küme üç bilgiyi (yineleme, sıra, konum erişimi) birden düşürüyor, kuyruk yalnızca konum erişimini düşürüyor.
- Üç arayüzün seçim ölçütü işlem hızı değil sözdür: liste sıra ve yineleme, küme tekillik, kuyruk uç vaat eder. Karmaşıklık karşılaştırması Veri Yapıları kursunda yapılmıştı ve burada tekrarlanmadı; burada ölçülen yalnızca hangi bilginin girdiden çıktığında hâlâ okunabilir olduğu.
- Kümenin düşürdüğü bilgi bir kusur değildir; tekillik sözünü tutmanın zorunlu bedelidir. Kuyruğun sırayı koruması ise arayüzün sözü değil, seçilen gerçekleştirimin tercihidir.
LinkedListhemListhemQueuearayüzünü aynı anda taşır ve iki arayüzünremovesözü tek bir çağrı yazımında çakışır: birebir aynı yazılanremove(1)bir yerde konumla, bir yerde değerle siler.- Çakışmada hiçbir garanti kırılmaz — iki arayüz de sözünü tutar, nesne değişmez. Kırılan
şey çağıranın yükümlülüğüdür ve kırılma sessizdir: istisna düşmez,
falsedöner ve dönüş değeri okunmazsa kusur fark edilmez.
Sonraki Adım
Bu derste liste, küme ve kuyruğun her biri, tuttuğu ögeye tek bir yoldan erişiyordu: konum, üyelik ya da uç. Bu üç erişim yolunun ortak noktası, kabın kendi kendine yetmesiydi — ögenin nerede olduğunu bulmak için kabın dışından hiçbir bilgi gerekmiyordu. Anahtar-değer eşleşmesi bu üçünden farklı bir söz veriyor: bir ögeye anahtarıyla erişilir ve arayüz o anahtarın karşılığını tekil tutacağını vaat eder. Ama anahtarın kendisinin ne olabileceğine dair sözün yarısı kütüphanede değil, çağıranın yazdığı sınıfta durur — kap yalnız anahtarı sorar, anahtarın kendi kendiyle tutarlı davranıp davranmayacağını denetlemez. Sıradaki ders bu yarıyı ölçer: bir anahtar sınıfı yanlış yazıldığında arama nereye düşer, hangi istisna fırlar ve fırlamıyorsa yerine ne olur.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.