İçeriğe geç
academia.sh

Ders 01 / 23

Kotlin'in Konumu

Kotlin'in JVM üzerindeki yerini ve tasarım hedeflerini tanıtır; aynı `==` işlecinin sekiz değerde dört ayrı ölçütle eşitlik verdiğini ölçer.

Yayın Güncelleme

Bu derste

  • Kotlin'in JVM üzerindeki konumunu ve statik tipli bir dil olarak amacını söylemek
  • `==` ve `===` işleçlerinin neyi karşılaştırdığını ayırt etmek
  • Bir değerin eşitliğinin tekil, kimlik, içerik ya da kısmi içerik ölçütlerinden hangisine dayandığını sınıflandırmak
İçindekiler

Bu kurs Kotlin’i bir dilin kendi sorusuyla öğretir: iki değer ne zaman aynıdır? Kotlin, statik tipli, derlenen bir programlama dilidir; JVM (Java Virtual Machine), Android, JavaScript, Wasm ve Native hedeflerine derlenir. Bu kurs ölçümlerini JVM hedefinde yapar: kaynak kodu bayt koduna çevrilir ve JVM’in kendi sınıf kitaplığıyla yan yana yaşar. Tasarım hedefleri iki cümlede toplanır: az törenle okunabilir kod yazmak ve tip denetimini derleme zamanına çekmek. Bu iki hedefin ortak noktası, dilin değerler hakkında kesin kurallar koymasıdır. İlk kurala hemen bakıyoruz: Kotlin’de == işleci eşitlik sorar, === işleci ise iki ifadenin aynı nesneye işaret edip etmediğini sorar.

Bu konunun sonraki yedi dersi aynı soruyu farklı yapılar üzerinde sorar. Her derste bir ölçüt bulunur ve bir sınırlayıcı ölçüm o ölçütün sınırını gösterir. Ölçüt dört addan biridir. Tekil (singleton) ölçüt, değerin bellekte tek bir örneği olduğu ve her yerden aynı nesneye ulaşıldığı durumdur. Kimlik ölçüt, eşitliğin nesnenin kendisine bağlı olduğu durumdur: aynı biçimde kurulmuş iki nesne eşit sayılmaz. İçerik ölçüt, eşitliğin taşınan değerlere bağlı olduğu durumdur. Kısmi içerik ölçüt, eşitliğin değerlerin yalnız bir bölümüne baktığı, geri kalanını yok saydığı durumdur.

Sekiz Değer, Dört Ölçüt

Ölçüm her derste aynı yardımcı dosyayla yapılır. Criterion sınıfı bir değer ile onun aynı biçimde kurulmuş ikizini, bir de farklı bir değeri alır; üç soru sorar. İkiz aynı nesne mi? İkiz == ile eşit mi? Farklı değer == ile eşit mi? Bu üç yanıt dört ölçütten birini belirler.

// 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 }}")
    }
}

Aşağıdaki program sekiz değeri bu sınıftan geçirir. Numaralandırma girdisi, nesne bildirimi, sıradan sınıf, dizi, veri sınıfı, liste, dizgi ve gövdesinde fazladan bir özellik taşıyan bir veri sınıfı. Veri sınıfı (data class), eşitliği ve metin gösterimini kurucu özelliklerinden türeten sınıftır; ayrıntısı bu dersin konusu değil, Veri Sınıfları dersinde işlenir; burada yalnız eşitliğine bakıyoruz.

// Main.kt — sekiz deger, dort olcut
enum class Color { RED, BLUE }

object Registry
object Archive

class Box(val size: Int)

data class Point(val x: Int, val y: Int)

data class Tag(val name: String) {
    var note: String = ""
}

fun main() {
    val word = listOf("kot", "lin").joinToString("")
    val checks = listOf(
        Criterion("numaralandirma-girdisi", Color.RED, Color.valueOf("RED"), Color.BLUE),
        Criterion("nesne-bildirimi", Registry, Registry, Archive),
        Criterion("siradan-sinif", Box(1), Box(1), Box(2)),
        Criterion("dizi", intArrayOf(1, 2), intArrayOf(1, 2), intArrayOf(1, 3)),
        Criterion("veri-sinifi", Point(1, 2), Point(1, 2), Point(1, 3)),
        Criterion("liste", listOf(1, 2), listOf(1, 2), listOf(1, 3)),
        Criterion("dizgi", "kotlin", word, "kotlim"),
        Criterion(
            "govdedeki-ozellik",
            Tag("a").apply { note = "ilk" },
            Tag("a").apply { note = "ilk" },
            Tag("a").apply { note = "son" },
        ),
    )
    for (check in checks) {
        check.report()
    }
    summarize(checks)
}
numaralandirma-girdisi: ayni-nesne=true, ikiz=true, farkli=false -> tekil
nesne-bildirimi: ayni-nesne=true, ikiz=true, farkli=false -> tekil
siradan-sinif: ayni-nesne=false, ikiz=false, farkli=false -> kimlik
dizi: ayni-nesne=false, ikiz=false, farkli=false -> kimlik
veri-sinifi: ayni-nesne=false, ikiz=true, farkli=false -> icerik
liste: ayni-nesne=false, ikiz=true, farkli=false -> icerik
dizgi: ayni-nesne=false, ikiz=true, farkli=false -> icerik
govdedeki-ozellik: ayni-nesne=false, ikiz=true, farkli=true -> kismi-icerik
tekil: 2
kimlik: 2
icerik: 3
kismi-icerik: 1

Çıktının okunuşu şöyledir. numaralandirma-girdisi ve nesne-bildirimi tekildir: numaralandırma girdisi (enum entry) ile object bildirimi bellekte tek örnek olarak yaşar, Color.valueOf("RED") çağrısı da aynı örneği döndürür. siradan-sinif ile dizi kimliktir: iki Box(1) aynı kurucu argümanını taşısa da eşit sayılmaz, iki intArrayOf(1, 2) de öyle. Dizide == elemanlara bakmaz, nesneyi karşılaştırır. veri-sinifi, liste ve dizgi içeriktir: ikizi eşittir, farklı değer eşit değildir ve ikizi aynı nesne değildir. Son satır, govdedeki-ozellik, kısmi içeriktir. note özelliği veri sınıfının kurucusunda değil gövdesinde tanımlı olduğu için eşitliğe katılmaz; note değeri ilk ile son olan iki nesne == ile eşit çıkar.

Özet satırları bu dağılımı doğrular: iki tekil, iki kimlik, üç içerik, bir kısmi içerik. Sekiz değerin tamamı sınıflandı ve hiçbiri beşinci bir ölçüte düşmedi.

DT1. Sekiz değerin her biri tek bir kurucu argümanı kümesiyle ölçüldü; aynı sınıfın başka argümanlarla kurulmuş örneklerinin sınıflandırması bu dersin dışındadır.

DT2. Dizi için yalnız intArrayOf ölçüldü; diğer dizi tiplerinin aynı davranışta olduğu bu derste ölçülmüş bir iddia değildir.

JVM Üzerinde Kotlin

Kotlin JVM üzerinde çalıştığı için JVM’in kendi tiplerini doğrudan kullanır. java.util.ArrayList böyle bir tiptir. Kotlin’in listOf ile kurduğu liste de çalışma anında bir JVM listesidir; ikisi aynı eşitlik kuralına bağlıdır. Aşağıdaki blok üç şeyi ölçer: iki JVM listesini, bir Kotlin listesi ile bir JVM listesini ve kurucuya geçirilen değeri değiştirince veri sınıfının eşitliğini.

// Main.kt — JVM listesi ve ayni veri sinifi
import java.util.ArrayList

data class Tag(val name: String) {
    var note: String = ""
}

fun main() {
    val jvmList = ArrayList<Int>()
    jvmList.add(1)
    jvmList.add(2)
    val jvmTwin = ArrayList<Int>()
    jvmTwin.add(1)
    jvmTwin.add(2)
    println(jvmList.javaClass.name)
    val checks = listOf(
        Criterion("jvm-listesi", jvmList, jvmTwin, listOf(1, 3)),
        Criterion("kotlin-ve-jvm-listesi", listOf(1, 2), jvmList, listOf(1, 3)),
        Criterion("veri-sinifi-yapici", Tag("a"), Tag("a"), Tag("b")),
    )
    for (check in checks) {
        check.report()
    }
    summarize(checks)
}
java.util.ArrayList
jvm-listesi: ayni-nesne=false, ikiz=true, farkli=false -> icerik
kotlin-ve-jvm-listesi: ayni-nesne=false, ikiz=true, farkli=false -> icerik
veri-sinifi-yapici: ayni-nesne=false, ikiz=true, farkli=false -> icerik
tekil: 0
kimlik: 0
icerik: 3
kismi-icerik: 0

İlk satır JVM’in sınıf adını basar: java.util.ArrayList. jvm-listesi içeriktir, çünkü aynı elemanları taşıyan iki liste eşittir ve ayrı nesnelerdir. kotlin-ve-jvm-listesi de içeriktir: Kotlin’in listOf(1, 2) ifadesiyle kurduğu liste, bir JVM listesiyle elemanlar aynıysa == ile eşit çıkar. Kotlin tipi ile JVM tipi eşitlik açısından ayrı bir dünya oluşturmaz. veri-sinifi-yapici ise bu derste daha önce gördüğümüz Tag sınıfının kurucuya bağlı hâlidir; kurucudaki name farklı olunca eşitlik bozulur, yani eşitlik kurucu özelliklerine bakar.

DT3. Bu bölümde listOf ile java.util.ArrayList yalnız eşitlik açısından karşılaştırıldı; listOf ifadesinin çalışma anı sınıfı ilerideki bir bölümde ayrıca basılır.

DT4. JVM listesi java.util.ArrayList ile ölçüldü; JVM’in diğer koleksiyon sınıfları bu dersin dışındadır.

Veri Sınıfının Türettikleri

Veri sınıfı, birincil yapıcıdaki özelliklerden equals, hashCode, toString, componentN ve copy üyelerini türetir; bu bölüm ilk üçünü ölçer. Önceki ölçüm yalnız == kolunu gösterdi. Aşağıdaki blok, gövdesinde note özelliği taşıyan bir Tag sınıfıyla üçünü birden sorar. İkinci nesnenin note değeri sonradan değiştirilir; kurucu argümanı iki nesnede de aynıdır.

// Main.kt — veri sinifi: equals, hashCode ve toString
data class Tag(val name: String) {
    var note: String = ""
}

fun main() {
    val first = Tag("a")
    val second = Tag("a")
    second.note = "ikinci"
    println(first == second)
    println(first.hashCode() == second.hashCode())
    println(first)
    println(second)
    println(first.note.isEmpty())
}
true
true
Tag(name=a)
Tag(name=a)
true

İlk iki satır true verir: == ve hashCode farkı görmez. Sonraki iki satır, println ile basılan gösterimin note değerini taşımadığını ortaya koyar. second.note artık "ikinci" olduğu hâlde ekrana Tag(name=a) çıkar. Son satır, first.note değerinin hâlâ boş olduğunu doğrular; yani iki nesne gerçekten farklı durumda ve türetilen üç davranış bu farkı birlikte yok sayar.

Sık yapılan hata tam burada çıkar. Bir hata ayıklama oturumunda iki nesneyi println ile basıp aynı görünce “bunlar aynı” sonucuna varılır; oysa gövdedeki özellik ayrı durur. Bu yüzden eşitliğe katılması gereken her değer kurucu özelliği olmalıdır. Java’da equals ve hashCode elle yazılır ve ikisinin birbiriyle tutarlı kalması geliştiricinin işidir; Kotlin’de ikisi aynı kurucu özelliklerinden birlikte türediği için, biri elle yeniden tanımlanmadıkça tutarsız kalmaz.

DT5. Java karşılaştırması bu bölümde ölçülmedi; Java tarafındaki equals ve hashCode yazımı bilinen davranış olarak anıldı, bu derste çalıştırılmış bir iddia değildir.

Çalışma Anı Sınıfları

Kotlin kaynağında tek bir Int adı vardır; JVM’de ise int ilkeli ile java.lang.Integer kutusu ayrı iki şeydir. Aynı değerin çalışma anında hangi sınıfla yaşadığını javaClass.name ile okuyabiliriz. Blok beş değerin ve üç listenin sınıf adını basar.

// Main.kt — Kotlin degerlerinin JVM siniflari
fun main() {
    val number: Any = 1
    val text: Any = "kotlin"
    println(number.javaClass.name)
    println(text.javaClass.name)
    println(listOf(1, 2).javaClass.name)
    println(listOf(1).javaClass.name)
    println(emptyList<Int>().javaClass.name)
    println(mutableListOf(1).javaClass.name)
    println(intArrayOf(1).javaClass.name)
}
java.lang.Integer
java.lang.String
java.util.Arrays$ArrayList
java.util.Collections$SingletonList
kotlin.collections.EmptyList
java.util.ArrayList
[I

Any tipli tutulan 1, java.lang.Integer olarak çıkar: değer bir Any değişkenine girince kutulanır. Dizgi java.lang.String olarak görünür; Kotlin’in ayrı bir dizgi sınıfı yoktur. intArrayOf(1) için sınıf adı [I çıkar, bu JVM’in int[] gösterimidir.

Listeler daha öğreticidir. listOf(1, 2) ifadesi java.util.Arrays$ArrayList verir, listOf(1) ise java.util.Collections$SingletonList. Boş liste için kotlin.collections.EmptyList çıkar. mutableListOf(1) ise java.util.ArrayList döndürür. Mekanizmanın nedeni şudur: listOf tek bir sınıf kurmaz, eleman sayısına göre en ucuz gerçekleştirmeyi seçer. Dolayısıyla bir List değerinin çalışma anı sınıfına güvenen kod kırılgandır: dört ayrı çağrı dört ayrı sınıf verdi. Eşitlik ise sınıf adına değil elemanlara dayanır; önceki bölümde listOf ile java.util.ArrayList içerik ölçütüyle eşit çıkmıştı.

Java karşılaştırması: Java’da int ile Integer kaynak kodda ayrı yazılır ve geliştirici kutulamayı kendisi seçer. Kotlin’de seçimi derleyici yapar. Bu gözlem Any tipli değişkenle ölçüldü; Int? tipinde de kutulama olacağı bu derste ölçülmedi.

DT6. Sınıf adları Kotlin 2.4.21 ve sandbox imajındaki JVM ile ölçüldü; başka bir JVM sürümünde listOf aynı iç sınıfları seçmeyebilir.

Null ile Karşılaştırma

== işleci boş değer alabilen tiplerde de çalışır. Aşağıdaki blok boş bir Tag? ile dolu bir Tag? arasındaki beş karşılaştırmayı yapar.

// Main.kt — null ile karsilastirma
data class Tag(val name: String)

fun main() {
    val missing: Tag? = null
    val present: Tag? = Tag("a")
    println(missing == null)
    println(missing == present)
    println(present == Tag("a"))
    println(missing === missing)
    val none: String? = null
    println(none == none)
}
true
false
true
true
true

Beş satır sırayla şunları söyler. Boş değer null ile eşittir. Boş değer dolu bir nesneyle eşit değildir ve bu karşılaştırma bir hata fırlatmaz. Dolu nesne, aynı kurucu argümanıyla kurulmuş başka bir Tag ile eşittir. === null için de çalışır. İki boş dizgi birbirine eşittir.

Mekanizma şudur: a == b ifadesi, a boşsa bnin de boş olup olmadığını sorar; a doluysa a.equals(b) çağrılır. Derleyici bu null denetimini kendisi ekler. Sık yapılan hata, bu denetimi elle yazmaktır: a != null && a.equals(b) biçimindeki ifade fazladan söz taşır. Java’da a.equals(b) çağrısı a boş olduğunda NullPointerException fırlatır; Java’da null güvenli karşılaştırma için Objects.equals kullanılır. Kotlin’de aynı güvence == işlecinin kendisindedir.

DT7. Bu beş karşılaştırma Tag ve String üzerinde ölçüldü; Java tarafındaki NullPointerException davranışı ve Objects.equals anımsatma olarak yazıldı, bu derste çalıştırılmadı.

Ölçümlerden Çıkan Alışkanlıklar

Bu dersin ölçümleri üç alışkanlığa bağlanabilir. Birincisi, bir sınıfın eşitlik davranışını tahmin etmek yerine ölçmektir. Tag veri sınıfı olduğu için iki ayrı örneği == ile eşit çıktı, ama === ile eşit çıkmadı; sıradan bir sınıfta ikisi de false verirdi. Bir sınıfı koleksiyonda anahtar olarak kullanacak ya da testte assertEquals ile karşılaştıracak kişi, önce bu iki işleç arasındaki farkı bilmelidir. Java’da == kimliğe, equals içeriğe bakar; Kotlin’de == içeriğe, === kimliğe bakar. Java’dan gelen biri == yazarken kimlik karşılaştırdığını sanabilir ve bu, dilleri ilk geçişte kod okumayı yanıltan bir noktadır.

İkincisi, çalışma anı sınıfına güvenmemektir. listOf çağrıları bu derste farklı JVM sınıfları üretti: tek öğeli liste, boş liste ve çok öğeli liste ayrı gerçekleştirimlerle geldi. Bu sınıf adları bir dil sözleşmesi değildir; Kotlin sürümü ya da hedef değiştiğinde değişebilir. Koda list is ArrayList gibi bir denetim yazmak, bu yüzden kırılgandır. Sözleşme, List arayüzünün kendisidir.

Üçüncüsü, null karşılaştırmasında okunur yazımı seçmektir. == ile null denetimi, nesnenin equals yöntemi çağrılmadan önce null durumunu ayrı ele alır; bu yüzden null == null doğrudur ve x == null yazımı NullPointerException fırlatmaz. Java’da x.equals(y) yazıldığında x boşsa istisna oluşur. Kotlin bunu işlecin tanımıyla kapatır ve ölçüm de beş karşılaştırmanın hiçbirinde istisna olmadığını gösterdi.

Bu üç alışkanlığın ortak yanı, ölçülmeden iddia edilmemesidir. Dersin geri kalanı bu ilkeyle ilerler: her iddia ya bir çıktıya dayanır ya da DT satırında “ölçülmedi” diye işaretlenir. Okuyucu için pratik sonuç şudur: bir davranıştan emin değilseniz, tek dosyalık küçük bir program yazıp çalıştırmak, belgeyi aramaktan daha kısa sürer. Bu kursta her dersin bir ölçütü olmasının nedeni de budur: ölçüt, hangi soruyu hangi işleçle sorduğunuzu baştan sabitler ve çıktıyı yorumlamayı kolaylaştırır. Veri sınıfı için içerik, sıradan sınıf için kimlik, tekil nesne için tekillik ölçütü geçerlidir. Bir tipin ölçütünü bilmek, o tiple yazılan her karşılaştırmanın ne anlama geldiğini bilmek demektir. Ölçütü yanlış varsayan kod çoğu zaman derlenir ve çalışır; hata ancak iki nesne beklenmedik biçimde eşit ya da farklı çıktığında görünür.

Sınırlayıcı Ölçüm

Ölçütün sınırı veri sınıfının içinde geçer. Aynı Tag sınıfı iki ölçütü birlikte taşır: kurucudaki name eşitliğe katılır, gövdedeki note katılmaz. Tag("a") ile Tag("a") içeriktir, Tag("a") ile Tag("b") eşit değildir; ama name aynı kalıp yalnız note değişince == yine true verir, yani kısmi içeriğe düşeriz. Bir sınıfın ölçütü bu yüzden sınıfın adından değil, equals davranışını hangi özelliklerden kurduğundan çıkar.

Bu iddiayı iki ölçümde gördük: veri-sinifi-yapici içeriktir, govdedeki-ozellik kısmi içeriktir.

Özet

  • Kotlin statik tipli, derlenen bir dildir; birden çok hedefe derlenir ve bu kursun ölçtüğü JVM hedefinde JVM tipleriyle doğrudan çalışır.
  • == eşitliği, === ise nesne kimliğini sorar.
  • Eşitlik dört ölçütten birine dayanır: tekil, kimlik, içerik, kısmi içerik.
  • Veri sınıfının eşitliği kurucu özelliklerine bakar; gövdedeki özellik eşitliğe katılmaz.
  • Bu dersin ölçütü: içerik; sınırlayıcı ölçüm: kurucudaki name farklı olunca aynı veri sınıfı eşit değildir.

Sonraki Adım

Eşitliğin dört ölçütünü gördük. Sıradaki derste en küçük yapıdan başlıyoruz: bir değişkenin adı, tipi ve yeniden atanabilirliği. val ile var arasındaki fark, aynı değerin iki değişkende nasıl davrandığını da değiştirir mi, onu ölçeriz.

İ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