Ders 18 / 23
Koleksiyon Türleri
Salt okunur arayüzün değiştirilemezlik anlamına gelmediğinin, listenin içeriğiyle, kümenin ve eşlemin sıradan bağımsız eşitliğinin ölçümü.
Yayın Güncelleme
Bu derste
- Salt okunur List arayüzünün aynı nesnenin değiştirilebilir bir görünümü olabildiğini ölçer
- Listenin eşitliğini türüne değil içeriğine dayandırdığını çıktıdan okur
- Kümenin ve eşlemin eşitliğinin eleman sırasından bağımsız olduğunu gösterir
- Eşlemde eksik anahtarın null döndürdüğünü, getValue çağrısının ise istisna fırlattığını ayırır
İçindekiler
Önceki ders “Genellikler ve Varyans” tip parametresinin çalışma anında silindiğini gösterdi. Bu derste o tip parametresini en çok taşıyan yapıların, yani koleksiyonların (collection) üzerinden gideceğiz. Bir program hemen her zaman birden çok değer üzerinde çalışır: sipariş satırları, kullanıcı adları, ayar anahtarları. Kotlin bunlar için üç temel koleksiyon sunar: sıralı ve tekrara izin veren liste (List), tekrara izin vermeyen küme (Set) ve anahtardan değere giden eşlem (Map).
Üçünün de iki yüzü vardır. List, Set ve Map arayüzleri yalnız okuma işlemlerini bildirir: eleman sayısı, elemana erişim, içerme sorusu. Eleman ekleyip çıkaran işlemler MutableList, MutableSet ve MutableMap arayüzlerindedir. Bu ayrımın dilde iki adı vardır ve karıştırılmaması gerekir. Koleksiyon salt okunur ise elimizdeki arayüz yazma işlemi sunmaz. Koleksiyon değiştirilemez ise nesnenin kendisi hiçbir yolla değişmez. İlki bir arayüz özelliğidir, ikincisi bir nesne özelliği. Ders bu farkı ölçer, sonra iki koleksiyonun eşit sayılmasını neyin belirlediğini sorar.
Kursun bu konusundaki ortak ölçü sorusu şudur: bu iki değeri eşit yapan ne? Dört ölçüt vardır. Eşit çıkan iki değer aynı nesneyse ölçüt tekildir. İkiz ayrı bir nesneyse ve eşit değilse kimliktir. İkiz eşitse ve yalnız tek bakımdan farklı olan değer de eşit değilse içeriktir. Farklı olan değer de eşit çıkıyorsa kısmi içeriktir. Her ölçümde üç değer kurulur: first, aynı tarifle ayrı kurulmuş ikizi twin ve yalnız bir bakımdan farklı olan other. Aşağıdaki Criterion sınıfı bu üç değeri === ve == ile karşılaştırır, ölçütü adlandırır ve satır olarak basar. Sınıf bir kez tanımlanır, her ölçüm onu kullanır; kural sonucu görür, gerekçeyi görmez.
// Criterion.kt — iki degeri esit yapan olcutu siniflandirir
class Criterion(val label: String, first: Any?, twin: Any?, other: Any?) {
val sameObject = first === twin
val equalTwin = first == twin
val equalOther = first == other
val name: String = when {
sameObject -> "tekil"
!equalTwin -> "kimlik"
!equalOther -> "icerik"
else -> "kismi-icerik"
}
fun report() {
println("$label: ayni-nesne=$sameObject, ikiz=$equalTwin, farkli=$equalOther -> $name")
}
}
fun summarize(checks: List<Criterion>) {
for (name in listOf("tekil", "kimlik", "icerik", "kismi-icerik")) {
println("$name: ${checks.count { it.name == name }}")
}
}
Bu bloğun çıktısı yoktur: yalnızca ölçüm aracını tanımlar ve sonraki her blokla birlikte derlenir.
Salt Okunur Arayüz, Aynı Nesne
val view: List<Int> = editable satırı yeni bir liste kurmaz. Soldaki tip yalnız hangi işlemlerin görüneceğini söyler; sağdaki nesne mutableListOf ile kurulmuş değiştirilebilir listedir. Dolayısıyla view üzerinden eleman eklenemese de editable üzerinden eklenen eleman view’da görünür. Gerçek bir kopya için toList() kullanılır: çağrı o anın elemanlarıyla yeni bir liste kurar ve sonraki değişiklikler ona ulaşmaz. Aşağıdaki blok bunu üç sorudan okur: view eklemeden sonra ne gösteriyor, snapshot ne gösteriyor ve ikisi editable ile aynı nesne mi?
// Main.kt — salt okunur arayuz ayni nesnenin gorunumudur
fun main() {
val editable = mutableListOf(1, 2)
val view: List<Int> = editable
val snapshot = editable.toList()
editable.add(3)
println(view)
println(snapshot)
println(view === editable)
println(snapshot === editable)
println((view as MutableList<Int>).size)
}
[1, 2, 3] [1, 2] true false 3
İlk satır eklemenin view’da göründüğünü, ikinci satır snapshot’ın eklemeden önceki iki elemanda kaldığını gösterir. Üçüncü satır view’ın editable ile aynı nesne olduğunu, dördüncü satır kopyanın ayrı bir nesne olduğunu söyler. Son satır daha rahatsız edici bir sonuç verir: salt okunur tipli view, MutableList’e dönüştürülebilir ve boyutu 3 olarak okunur. Yani salt okunur bir tip, değiştirilemezliğin garantisi değildir; yalnız o referans üzerinden yazma işleminin yapılamayacağını belirtir. Bir fonksiyon List döndürüyorsa ve içeride değiştirilebilir bir listeyi olduğu gibi veriyorsa, çağıran kendi liste sanıp onu tutarken içeriğin altından kayabilir. Güvenli yol, sınırı aşarken toList() ile kopya vermektir.
KE1. Bu ölçüm mutableListOf ile kurulan liste için. listOf(...) ile kurulan listenin MutableList’e dönüştürülüp değiştirilmeye çalışılması ve diğer koleksiyon kurucularının bu bakımdan davranışı ayrıca ölçülmedi.
Ölçüt: İçerik
İki listeyi neyin eşit yaptığını soralım. Aklımıza gelen ilk cevap türdür: biri MutableList, öbürü List ise farklıdır. Ölçüm tersini gösterir. Aşağıdaki blok önce aynı elemanlı değiştirilebilir ve salt okunur bir listeyi, sonra listOf ile arrayListOf ile kurulanı == ile karşılaştırır. Ardından Criterion ile aynı ölçüyü sınıflandırır: first ve twin aynı tarifle (mutableListOf(1, 2)) ayrı kurulur, other ise yalnız son elemanı farklı bir listedir.
// Main.kt — liste turu degil icerik karsilastirilir
fun main() {
println(mutableListOf(1, 2) == listOf(1, 2))
println(listOf(1, 2) == arrayListOf(1, 2))
val check = Criterion("liste", mutableListOf(1, 2), mutableListOf(1, 2), listOf(1, 3))
check.report()
}
true true liste: ayni-nesne=false, ikiz=true, farkli=false -> icerik
İlk iki satır, türü ve gerçeklemesi farklı listelerin aynı elemanlarla eşit çıktığını gösterir. Üçüncü satır ölçütü adlandırır: ikiz ayrı bir nesne olduğu için tekil değil, ikiz eşit olduğu için kimlik değil, farklı eleman eşit çıkmadığı için kısmi içerik değil. Kalan ölçüt içeriktir. Bu dersin ölçütü budur: iki liste, aynı sırada aynı elemanları taşıyorsa eşittir; hangi arayüzle görüldükleri ya da hangi sınıftan geldikleri sonucu değiştirmez. Bu, yapısal eşitliğin (==) koleksiyonlarda equals çağrısını elemanlara kadar götürmesinin sonucudur. Kimlik eşitliği (===) ise iki ayrı listede her zaman yanlış çıkar ve burada hiç belirleyici olmaz.
KE2. Bu ölçüm iki elemanlı Int listeleri için. Elemanları kendi equals’ını tanımlamayan sınıflardan oluşan listelerin eşitliği ayrıca ölçülmedi; o durum bir sonraki dersin konusudur.
Sınırlayıcı Ölçüm
İçerik ölçütü listede sıra bilgisini de içerir: listOf(1, 2) ile listOf(2, 1) aynı elemanlara sahip olsa da eşit değildir. Küme ve eşlem için aynı soruyu sorarsak ölçütün sınırını buluruz. Aşağıdaki blok beş ölçümü tek seferde basar. Listede other ters sıradadır. Kümede ve eşlemde de other yalnız sırası farklı bir koleksiyondur; eslem-deger ölçümünde ise other bir değeri farklıdır. Son ölçümde boş iki listenin tip parametresi bilerek farklı tutulur: emptyList<Int>() ve emptyList<String>().
// Main.kt — kumede ve eslemde sira fark degildir
fun main() {
val checks = listOf(
Criterion("liste", listOf(1, 2), listOf(1, 2), listOf(2, 1)),
Criterion("kume", setOf(1, 2), setOf(1, 2), setOf(2, 1)),
Criterion("eslem", mapOf("a" to 1, "b" to 2), mapOf("a" to 1, "b" to 2), mapOf("b" to 2, "a" to 1)),
Criterion("eslem-deger", mapOf("a" to 1, "b" to 2), mapOf("a" to 1, "b" to 2), mapOf("a" to 1, "b" to 3)),
Criterion("bos-liste", emptyList<Int>(), emptyList<Int>(), emptyList<String>()),
)
for (check in checks) check.report()
summarize(checks)
}
liste: ayni-nesne=false, ikiz=true, farkli=false -> icerik kume: ayni-nesne=false, ikiz=true, farkli=true -> kismi-icerik eslem: ayni-nesne=false, ikiz=true, farkli=true -> kismi-icerik eslem-deger: ayni-nesne=false, ikiz=true, farkli=false -> icerik bos-liste: ayni-nesne=true, ikiz=true, farkli=true -> tekil tekil: 1 kimlik: 0 icerik: 2 kismi-icerik: 2
Sınırlayıcı ölçüm şudur: kümede ve eşlemde sıra fark değildir. Listede sıra değişince eşitlik bozulur (icerik), kümede ve eşlemde aynı değişiklik hiçbir fark yaratmaz (kismi-icerik): farklı olan değer de eşit çıkar. Küme eşitliği aynı eleman topluluğuna bakar, sıra bunun parçası değildir; eşlem ise anahtar kümesi ve her anahtarın değeriyle karşılaştırılır, anahtarların yazılış sırası bunun dışındadır. eslem-deger satırı bu ikincisini doğrular: sıra aynı kalıp bir değer değişince eşlem eşit çıkmaz. Dört ölçümün dağılımı 2/2 olarak iki ölçüte bölünür; beşinci ölçüm başka bir şeyi gösterir.
Beşinci satırdaki sonuç iki yönden dikkat çeker. emptyList<Int>() ile emptyList<String>() aynı nesnedir, dolayısıyla ölçüt tekildir ve hiç karşılaştırma gerektirmez. Dahası, bu boş liste tek ve ortak bir nesne olduğu için tip parametresi farklı iki çağrı da onu verir; tip parametresi çalışma anında silindiğinden other ile first eşit çıkar. Önceki dersteki tip silme burada bir koleksiyon eşitliğinin ayrıntısı olarak karşımıza çıkar.
KE3. Bu ölçümler iki elemanlı koleksiyonlar ve emptyList için. Daha büyük kümelerde karma kodunun çakışması durumundaki davranış ve kendi hashCode’unu tanımlamayan elemanlı kümeler ayrıca ölçülmedi.
Eşlem ve Küme İşlemleri
Eşlem, günlük işte listeden de sık kullanılır, ama okuma kuralı iki yerde şaşırtır. Bir anahtar yoksa [] ile okumak null döndürür; bu yüzden sonuç tipi boş değer alabilen bir tiptir (Int?). getOrDefault aynı durumda verdiğiniz değeri döndürür. getValue ise eksik anahtarda bir istisna (NoSuchElementException) fırlatır. Aşağıdaki blok bu üç okumayı, yazma işlemlerini ve + işleciyle yeni bir eşlem kurmayı bir arada ölçer. stock + ("muz" to 2) mevcut eşlemi değiştirmez, içinde yeni anahtar bulunan ayrı bir eşlem döndürür. Son iki satır hashSetOf ile kurulan kümeyi gösterir: bu kümenin yineleme sırası bir garanti değildir, bu yüzden elemanlar sıralanarak basılır.
// Main.kt — eslem okumalari
fun main() {
val stock = mutableMapOf("elma" to 3, "armut" to 5)
println(stock["muz"])
println(stock.getOrDefault("muz", 0))
stock["elma"] = 4
stock.put("kiraz", 1)
println(stock.keys)
println(stock.values.sum())
val copy = stock + ("muz" to 2)
println(stock.size)
println(copy.size)
try {
stock.getValue("muz")
} catch (e: NoSuchElementException) {
println(e::class.simpleName)
}
val tags = hashSetOf("b", "a", "b")
println(tags.size)
println(tags.sorted())
}
null 0 [elma, armut, kiraz] 10 3 4 NoSuchElementException 2 [a, b]
Sırayla okuyalım. Eksik anahtar null ve verilen varsayılan olarak 0 döndü. stock["elma"] = 4 var olan anahtarın değerini değiştirdi, put yeni bir anahtar ekledi; anahtarlar elma, armut, kiraz sırasında kaldı, değerler 4 + 5 + 1 olarak 10 toplandı. + ile kurulan copy dört girdi taşırken stock üç girdide kaldı: yeni eşlem ayrıdır. getValue eksik anahtarda istisnayı fırlattı ve yakalandı. Son iki satır, "b" iki kez eklendiği hâlde kümenin iki elemanlı olduğunu ve sıralanmış hâlinin [a, b] olduğunu gösterir.
Buradan çıkan kural pratiktir: eksik olması beklenen bir anahtar için [] kullanılır ve sonuç ?: (Elvis işleci) ile bir varsayılana bağlanır; eksik olmaması gereken bir anahtar için getValue kullanılır ve eksiklik bir hata olarak yüzeye çıkar. Bu iki yol arasındaki seçim aynı zamanda eşlemin sözleşmesidir: okunan değerin var olduğuna program mı karar veriyor, veri mi?
KE4. Bu ölçümler mutableMapOf ile kurulan eşlem ve hashSetOf ile kurulan küme için. mutableMapOf ile kurulan eşlemin yineleme sırası bu ölçümde eklenme sırasıyla örtüştü; bu bir dil garantisi olarak ölçülmedi ve hashMapOf için sıra hiç iddia edilmedi.
Dolaşırken Değiştirmek
En sık yapılan koleksiyon hatalarından biri, bir listeyi dolaşırken aynı listeden eleman silmektir. for (x in list) döngüsü arkada bir yineleyici (iterator) kullanır ve yineleyici, kendisi dışında bir yolla değişen koleksiyonu fark edince çalışmayı reddeder. Aşağıdaki blok dört elemanlı bir listede çift sayıları döngü içinde silmeye çalışır; fırlatılan istisnanın adını ve listenin o andaki durumunu basar. Sonra aynı iş removeAll ile yapılır. Son iki satır, liste ile kümenin aynı elemanlarla bile eşit sayılmadığını ve bir listeden kurulan kümenin sıradan bağımsız eşit çıktığını gösterir.
// Main.kt — dolasirken silmek
fun main() {
val numbers = mutableListOf(1, 2, 3, 4)
try {
for (n in numbers) {
if (n % 2 == 0) numbers.remove(n)
}
} catch (e: ConcurrentModificationException) {
println(e::class.simpleName)
}
println(numbers)
numbers.removeAll { it % 2 == 0 }
println(numbers)
println(listOf(1, 2) == setOf(1, 2))
println(listOf(2, 1).toSet() == setOf(1, 2))
}
ConcurrentModificationException [1, 3, 4] [1, 3] false true
2 döngünün ikinci adımında silindi ve liste değişti; istisna bir sonraki next() çağrısında fırlatıldı. Liste bu yüzden [1, 3, 4] durumunda kaldı; 4 hiç denetlenmedi. Bir çift sayının hâlâ listede kalması, hatanın yalnızca istisnadan ibaret olmadığını, yarım kalmış bir temizlik bıraktığını gösterir. removeAll ise koşulu bütün elemanlara tek geçişte uygular ve [1, 3] bırakır. Kural şudur: bir koleksiyonu silerek temizlemek gerekiyorsa removeAll ya da retainAll gibi koleksiyonun kendi toplu işlemi kullanılır; dolaşma sırasında koleksiyonun kendisinden silme yapılmaz (yineleyicinin kendi remove yöntemi bunun dışındadır).
Son iki satır eşitliğin türe de bağlı olduğunu gösterir. Aynı elemanlara sahip List ile Set eşit değildir: liste eşitliği yalnız başka bir listeyle, küme eşitliği yalnız başka bir kümeyle tanımlıdır. Bu nedenle liste kümeye çevrildikten sonra karşılaştırma yapılır; çevrilmiş toSet() sıradan bağımsız eşit çıkar.
KE5. Bu ölçüm ArrayList tabanlı mutableListOf için. Silmenin listenin sondan ikinci elemanında yapıldığı durumda istisnanın fırlatılıp fırlatılmadığı ölçülmedi ve güvence olarak yazılmaz.
Değişen Elemanın Bedeli
Küme ve eşlem, elemanı ya da anahtarı bir sayı (karma kodu) ile yerleştirir. Eleman değiştirilebilir bir listeyse ve kümeye girdikten sonra değiştirilirse, sayı değişir ama küme eleman yerini güncellemez. Sonuç, kümede duran ama aranınca bulunamayan bir elemandır. Aşağıdaki blok bir listeyi kümeye ekler, listeye eleman ekler ve kümede iki biçimde arar: değiştirilmiş listenin kendisiyle ve aynı içerikte yeni kurulmuş bir listeyle. Boyut her durumda okunur.
// Main.kt — kumeye girdikten sonra degisen eleman
fun main() {
val key = mutableListOf(1)
val bag = hashSetOf(key)
println(bag.contains(key))
key.add(2)
println(bag.size)
println(bag.contains(key))
println(bag.contains(mutableListOf(1, 2)))
println(bag.any { it === key })
}
true 1 false false true
Eklemeden önce arama başarılıdır. Eklemeden sonra küme boyutu 1 kalır, yani eleman silinmedi; ama değiştirilmiş listeyle de, aynı içerikli yeni listeyle de arama başarısız olur. Son satır elemanın hâlâ orada olduğunu kanıtlar: kümeyi doğrudan dolaşınca aynı nesne bulunur. Eleman kümede durur, ama küme ona artık ulaşamaz. Bu hata sessizdir ve istisna fırlatmaz.
Çıkarılacak ders açık: küme elemanı ya da eşlem anahtarı olacak değerler girdikten sonra değişmeyen türlerden seçilir. String, sayılar ve alanlarının hepsi val olup kendileri de değiştirilemeyen veri sınıfları bu bakımdan güvenlidir; MutableList değildir. Değişen bir değer anahtar yapılacaksa önce toList() ile kopyası alınır ve kopya anahtar olur.
KE6. Bu ölçüm bir HashSet içindeki ArrayList elemanı için. LinkedHashSet ve ağaç tabanlı kümelerin aynı durumdaki davranışı ve eşlem anahtarı olarak değişen eleman ölçülmedi.
Özet
- Salt okunur
List,SetveMaparayüzleri değiştirilemezlik garantisi vermez; aynı nesneyi gösterenMutableListdeğişirse salt okunur görünüm de değişir. - Gerçek kopya için
toList()kullanılır; sınırı aşan bir fonksiyon değiştirilebilir bir koleksiyonu olduğu gibiListdiye döndürmemelidir. - Liste eşitliği türe değil içeriğe ve sıraya bakar. Bu dersin ölçütü: içerik.
- Sınırlayıcı ölçüm: küme ve eşlemde sıra fark değildir (
kismi-icerik); boş liste tekil bir nesnedir ve tip parametresi eşitliği etkilemez. - Eşlemde eksik anahtar
[]ilenull,getValueile istisna verir.
Sonraki Adım
Bu ders koleksiyonu bir veri kabı olarak ölçtü. Bir sonraki ders, o kabın içeriğinden yeni koleksiyonlar üreten işlemlere geçer: map, filter, fold, groupBy. Orada da aynı soru geri döner: aynı listeye aynı dönüşüm iki kez uygulanırsa sonuçlar neyle eşit sayılır?
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.