Kotlin - Değişkenler ve Veri Tipleri

Esra Avşar
11 min readJul 13, 2023

Herkese merhaba, Kotlin temelleri üzerinde yazacağım serinin ilk yazısıyla sizlerleyim. Bu yazımda, Kotlin’de değişkenlerin nasıl tanımlanacağını, veri tiplerini ve bunların nasıl kullanıldığını anlatıp son olarak da Nullables konusuna değinip yazımı noktalayacağım.

Öncelikle değişken ve veri tipi nedir bunu konuşalım.

Değişken Nedir? Değişken, bir isimle tanımlanır ve içerisine atayacağımız veri bellekte yer tutar. Kullanmak istediğimiz zamanda da belirlediğimiz isimle çağırıp işlem yapabiliriz. Değişkenleri isimlendirirken isimlendirme içerisinde olabildiğince val numberDouble gibi veri tipi ismi kullanmamalıyız çünkü tip dönüşümü yaptığımız zamanlarda değişkenin tipi değişse bile isminden dolayı kafamız karışabilir.

Değişkenler var -> değişken( variable ), ve val -> değer( value ) olmak üzere iki çeşit anahtar kelime kullanılarak tanımlanabilir.

Kotlin’de değişken tanımlarken aşağıdaki düzeni kullanırız. Fakat Kotlin’de değişkenlerin veri tiplerini otomatik olarak algılayan “type inference” mekanizması olduğu için veri tipi belirtmeden de değer ataması yapabiliriz.

val/var Değişken Adı: Veri Tipi = Değişken Değeri

Bir değişkeni val olarak tanımlarsak o değişkenin değerini daha sonra değiştiremeyiz( immutable ). Herhangi bir projede geliştirme yaparken değişkeni ilk olarak val tanımlayıp gerekli durumlara var’a dönüştürmek teknik borç(technical debt) durumuna yol açmamak için önemlidir.

val name = "Esra"
name = "Irmak" // Böyle bi atama yapmaya çalışırsak "Val cannot be reassigned" yani "tekrar değer atanamaz" hatası alırız.

Fakat var olarak tanımladığımız zaman o değişken üzerinde değişiklik yapabiliriz( mutable ).

var name = "Esra"
name = "Irmak" // Herhangi bir hata almadan atama işlemi gerçekleşir.

✨ Veri Tipi Nedir? Veri tipleri değişkenlerimizin hangi türde verileri temsil ettiğini ve üzerinde hangi işlemlerin yapılabileceğini belirleyen kavramlardır. Ben bu yazımda sizlere Numbers, Booleans, Characters, Strings, Arrays ve bunların haricinde de Unit ve Any başlıklarını anlatacağım.

Numbers( Sayılar )

Bu veri tipini Tam Sayılar ve Ondalık Sayılar olmak üzere iki farklı grupta inceleyebiliriz.

Tam sayılar için farklı boyutlara ve dolayısıyla değer aralıklarına sahip dört tür vardır:

Tam sayı çeşitleri

Değişkenlerimizi tanımlarken tür belirtmediğimiz sürece derleyici değeri temsil etmeye yetecek en küçük aralığa sahip türü otomatik olarak belirler. Eğer Byte veya Short veri tiplerini kullanmak istersek değişken isminin yanında tip belirtmemiz gerekir. Tam sayı atadığımız değişkenin değeri Int aralığını aşmıyorsa tür Int olarak belirlenir. Aşarsa Long’dur ve Long değerleri açıkça belirtmek için atamak istediğimiz değerin sonuna L’ son ekini eklememiz gerekir. Değişkenlerimize değer ataması yaparken doğru değişken tipini kullanmamız önemlidir.

val signalStrength: Byte = 3 // Byte
val age: Short = 35 // Short
val score = 100 // Int
val count = 15000000000 // Long -> Buradaki gibi Int değerini aşan değer atamalarında 'L' son ekini eklemesek de bu değer Long tipindedir.
val distance = 15000L // Long
val population: Long= 789_654_321 // Bu kullanım büyük değerli değişkenlerin okunabilirliğini arttırır. Burada tür belirtmezsek değişkenimiz otomatik olarak Int belirlenir.

Ondalık Sayılar için farklı boyutlara sahip iki tür vardır:

Ondalık sayı çeşitleri

Double ve Float türünde değişkenleri ondalık sayı ataması yapmak istediğimiz zaman kullanırız. Ondalık kısım, tam sayı kısmından bir nokta( . ) ile ayrılır. Ondalık sayılarla başlatılan değişkenler için, derleyici Double türünü kabul eder. Float türünü kullanmak istediğimizde ise açıkça o değerin Float olduğunu ‘f’ veya ‘F’ son eklerini kullanarak belirtebiliriz.

val pi = 3.1415926535897932 // Double -> "Floating-point literal cannot be represented with the required precision" uyarısı alırız ve Double tipini aşan kısımlar bu değişkeni ekrana yazdırmak istediğimizde fazla kısımlar silinmiş haliyle yazılır.
val temperature = 26.5f // Float

✨Tip Dönüşümü Nedir?

Tip dönüşümü, bir veri tipinin diğer bir veri tipine dönüştürülmesidir. Bu işlem örtük( implicit ) ve açık( explicit ) olmak üzere ikiye ayırılır.

Diğer bazı veri tiplerinden farklı olarak Kotlin’de sayılar için örtük dönüşüm yoktur.

Açık dönüşümde ise küçük tipler büyük tiplere kendisine yazılan genişletme( extension ) fonksiyonlarıyla çevrilirler. Bu konudan sonraki yazılarımda bahsedeceğim.

Açık tip dönüşümü genelde küçükten büyük olana dönüşüm şeklinde gerçekleşir. Tersi durumda büyük olan tipi küçüğe dönüştürmek istediğimizde veri kaybı gibi durumlar gerçekleşebilir.

Bunu şöyle bir gerçek hayat örneği üzerinden düşünebiliriz: saklama kabı gibi bir nesneyi buzdolabı içine kolayca yerleştirebiliriz fakat aksi durum mümkün değildir. Eğer buzdolabını saklama kabına sığdırmak gibi bir şey deneyecek olursak ancak buzdolabının saklama kabına sığabilecek büyüklükte parçalarını sığdırabiliriz.

val pi = 3.141592653589793 // Double
val score = 100 // Int


println("${pi.toInt()}") // output: 3 -> noktadan sonraki kısımlar silinir, veri kaybı oluşur.
println("${score.toLong()}") // output: 100 -> veri olduğu gibi ekrana yazdırılır.

Tüm sayı türleri, diğer türlere dönüşümü destekler:

toByte(): Byte,
toShort(): Short,
toInt(): Int,
toLong(): Long,
toFloat(): Float,
toDouble(): Double

Booleans

Boolean veri tipi doğru( true ) veya yanlış( false ) olmak üzere iki olası değere sahiptir. Bu veri tipi bir durumu sorgulamak veya mantıksal ifadelerin sonuçlarını temsil etmek için kullanılır.

Mantıksal operatörler şu şekildedir:

&& -> ve( and )

|| -> veya( or )

! -> değil( not )

Boolean veri tiplerini isimlendirirken genelde temsil ettikleri durumu açıkça ifade edecek şekilde olumlu ifadeler kullanmaya dikkat etmeliyiz. Örneğin, “isValid, hasPermission” gibi ifadeler Boolean bir değişkene verilebilecek türde isimlendirme şekilleridir.

Mantıksal operatörlerden ünlem( ! ) kullanmak yerine “not()” kullanmak kod okunabilirliğini attırır. Örneğin:

val isReady: Boolean = true
val isValid = false

if (!isReady) {}

if (isValid.not()) {}

Characters( Karakterler )

Char veri tipi tek tırnak( ‘ ’ ) içerisinde ifade edilen tek bir karakteri saklamak için kullanılır. Bu tip Latin alfabesindeki harfleri temsil edebildiği gibi, aynı zamanda diğer dillerdeki semboller ve rakamlar gibi özel karakterleri de temsil etmek için kullanılabilir. Bu veri tipine ait bazı kaçış karakterleri( escape sequences ) vardır.

  • \t ⇢ bir sonraki karakterin sekme boşluğunda başlaması sağlar.
  • \n ⇢ yeni satıra geçilmesini sağlar.
  • \r ⇢ satır başına dönme karakterini temsil eder.
  • \' ⇢ ( ) işaretinin ekrana yazılabilmesini sağlar.
  • \" ⇢ ( ) işaretinin ekrana yazılabilmesini sağlar.
  • \\ ⇢ ( \ ) ( backslash ) işaretinin ekrana yazılabilmesini sağlar.
  • \$ ⇢ ( $ ) işaretinin ekrana yazılabilmesini sağlar.
var firstLetter: Char = 'E'
val grade = 'A'

Karakterleri tanımlarken tek tırnak işareti( ‘ ’ ) içerisine birden fazla karakter yazmak veya çift tırnak işareti( “ ” ) içerisinde bir veya birden fazla karakter yazmak Char tanımlaması için yanlıştır. Eğer karakter çift tırnak işareti( “ ” ) içerisinde yazılırsa veri tipi String olur Char olmaz.

var firstLetter: Char = 'E' // Doğru kullanım.
var secondLetter: Char = "s" // Yanlış kullanım.
var thirdLetter: Char = 'ra' // Yanlış kullanım.

Char veri tipi kullanarak tip dönüşümü yapmak istersek dikkat etmemiz gereken önemli bir nokta vardır. Eğer Char tipindeki bir değeri Int tipine dönüştürmek istersek, değişken isminin sonuna .toInt() yazarak dönüşüm sağlayabiliriz. ANCAK! bu noktada dikkat etmemiz gereken bir durum vardır.

val strValue: Char = '7'

Örneğin yukarıdaki gibi bir değişken tanımladığımızda bu değişkene strValue.toInt() şeklinde bir tip dönüşümü yaptığımızda 7 sayısını elde etmeyi bekleriz ancak işler o şekilde ilerlemiyor.

println(strValue.toInt()) // outuput: 55

İyi de elimizde Char tipinde 7 değeri varken bunu Int tipine dönüştürmek istediğimizde neden 55 çıktısı üretiyor sorusunun cevabı ise ASCII tablosunda yer alıyor.

ASCII Tablosu

Derleyici .toInt() tip dönüşüm komutunu aldığı anda otomatik olarak Char olan 7 değerini tabloda arıyor ve o değerin onluk sayı sistemindeki karşılığını bize gösteriyor.

Strings( Dizeler )

String veri tipi metinsel ifadeleri veya karakter dizilerini temsil etmek için kullanılır. String değerlerini tırnak işareti( “ ” ) içerisinde tanımlamamız gerekir. Bir döngü ile String tipindeki verinin indekslerinde yer alan değerlere erişebiliriz. String’ler değiştirilemez( immutable ) yapılardır ve oluşturulduktan sonra üzerinde değişiklik yapılamaz. String’leri dönüştürebilen işlemleri kullandığımızda da orijinal halini değiştirmeden yeni bir nesne oluşur ve biz o nesneyi kullanarak değişimi gözlemleriz.

val firstName= "Esra"
println(firstName.uppercase()) // output: ESRA
println(firstName) // output: Esra

String’leri birleştirmek için ‘+’ işaretini kullanabiliriz.

val str1 = "to be " + "or "
val str2 = "not to be"
println(str1 + str2)
// output: to be or not to be

String’leri farklı tür değişkenlerle de birleştirebiliriz. Fakat bu noktada dikkat etmemiz gereken bir durum vardır. Farklı tip değişkenden önce String değişkenimizi yazarak birleştirme yapabiliriz fakat tam tersi durum mümkün değildir. Bunun mümkün olması için genişletme( extension ) fonksiyonları kullanmamız gerekir. Daha önce de söylediğim gibi gelecek yazılarımda bu konudan ayrıntılı bir şekilde bahsedeceğim. Şimdi, hadi gelin bir örnekte bu farklı türleri birleştirme konusunu inceleyelim:

val userName: String = "Esra"
val userAge = 25

val combinedStr = userName + userAge // Böyle bir birleştirme yapabiliriz.
println("Combined String : $combinedStr") // output: Combined String : Esra25

val combinedStr2 = userAge + userName // Bu satır hata verir.

String olmayan değerleri ve önceden oluşturulmuş String değerlerini değişken adının başına dolar işareti( $ ) koyarak ekrana yazı yazdırmak için kullanabiliriz. Eğer bu değişken bir etkileşime girecekse örneğin yanındaki bir değişkenle toplanacaksa bu işaretin yanında bir de süslü parantez( { } ) kullanmamız gerekir.

val firstFruitName = "Elma"
val firstFruitNumber = 3

val secondFruitName = "Kiraz"
val secondFruitNumber = 7

println("Sepette $firstFruitNumber tane $firstFruitName ve $secondFruitNumber tane $secondFruitName olmak üzere toplam ${firstFruitNumber + secondFruitNumber} tane meyve var.")
// output: Sepette 3 tane Elma ve 7 tane Kiraz olmak üzere toplam 12 tane meyve var.

Metin sabitleri( String literals ) sabit metinleri temsil etmek için kullanılır. Kotlin’de iki tür metin sabit değeri vardır.

✱ Kaçış Dizeleri( Escaped Strings )

Kaçış dizeleri, kaçış karakterlerini içerebilir. Kaçış karakterlerini Charactersbaşlığı altında incelemiştik. Hadi bir örnek verelim:

val escapedStr = "Bu bir kaçış dizesidir.\nİçinde bir yeni satır karakteri vardır"
println(escapedStr)

/*
output:

Bu bir kaçış dizesidir.
İçinde bir yeni satır karakteri vardır.
*/

✱ Ham Dizeler( Raw String )

Ham dizeler, üçlü çift tırnak ( “”” ) ile çevrelenir. Bu tip dizeler kaçış karakterlerini kullanmadan da kaçış karakterleri ile yapabileceğimiz işlemleri gerçekleştirebilmemizi sağlar. Bu dizelerin içerisinde kaçış karakterleri çalışmaz. Ham dizenin içeriği olduğu gibi korunur. Şöyle bir örnekle bunu pekiştirelim:

val rawStr= """
Bu bir ham dizedir.
İçinde \n gibi özel karakterler kullanılarak kaçış yapmaya gerek yoktur.
"""
println(rawStr)

/*
output:

Bu bir ham dizedir.
İçinde \n gibi özel karakterler kullanılarak kaçış yapmaya gerek yoktur.
*/

Ham dizelerin kenarlarında kalan boşlukları temizlemek için .trimIndent() metodunu kullanırız. Burada önemli olan nokta ham dize içerisindeki metnin en solundaki karakteri neredeyse ona göre bir silme işlemi uygular. Örneğin:

val rawStr2= """
-> Bu bir ham dizedir.
İçinde \n gibi özel karakterler kullanılarak kaçış yapmaya gerek yoktur.
""".trimIndent()
println(rawStr2)

/*
output:

-> Bu bir ham dizedir.
İçinde \n gibi özel karakterler kullanılarak kaçış yapmaya gerek yoktur.
*/

val rawStr3= """
Bu bir ham dizedir.
İçinde \n gibi özel karakterler kullanılarak kaçış yapmaya gerek yoktur.
""".trimIndent()
println(rawStr3)

/*
output:

Bu bir ham dizedir.
İçinde \n gibi özel karakterler kullanılarak kaçış yapmaya gerek yoktur.
*/

Arrays( Diziler )

Kotlin’deki diziler Array sınıf tarafından temsil edilir. Diziler, yazımın önceki kısmında üzerinde konuştuğumuz veri tiplerinin bir arada tutulmasını sağlayan yapıdır. Diziler, her değer için ayrı değişkenler oluşturmak yerine birçok değeri tek değişkende tutabilmemizi sağlar.

arrayOf(Aynı Tip Değişken Değerleri)

arrayOf<Any>(Farklı Tip Değişken Değerleri)

val numbers = arrayOf(1, 5, 3, 8, 4, 6)
val mixedValues = arrayOf<Any>(47, "Esra", 'A', false)

Array elemanlarına erişim sağlayabilmek için köşeli parantez ( [ ] ) kullanılması gerekir. Array indeksi 0'dan başlar ve son elemanın indeks değeri dizi uzunluğu - 1'dir.

val firstNumber = numbers[0] // ilk elemana erişim
val lastNumber = numbers[numbers.size-1] // son elemana erişim

Array içerisindeki elemanları değiştirmek için indeksine erişip içerisindeki değere başka bir değer ataması yapabiliriz. Dizimizi val olarak tanımlamamız buna engel değildir. Ancak dizinin kendisi başka bir dizi ile değiştirilemez. Bu yapabilmemiz için dizi tanımlamamızı var ile değiştirmemiz gerekir.

println("${numbers[3]}") // output: 8
numbers[3] = 2 // Üçüncü elemanı 2 olarak değiştirdik.
println("${numbers[3]}") // output: 2

Array indekslerine erişirken dizi boyutunu göz önünde bulundurmamız gerekir. Boyutu 4 olan bir dizide son indeks 3 iken eğer biz 4. indekse erişmeye çalışırsak ArrayIndexOutOfBoundException şeklinde bir hata alırız. Bizim örneğimize göre şu şekilde düşünebiliriz:

println("${numbers[5]}") // output: 6
println("${numbers[6]}") // hata: ArrayIndexOutOfBoundsException

Array içerisindeki elemanları döngüler yardımıyla tek tek gezip işlemler yapabiliriz. Döngülere bir sonraki yazımda değineceğim ama fikir olması açısından şöyle düşünebilirsiniz:

// Döngü sayesinde Array elemanlarını alt alta ekrana yazabiliriz.
for (number in numbers) {
println(number)
}

Ayrıca bazen kullanıcının kaç tane değer gireceğini bildiğimiz fakat girilecek değerlerin belirsiz olacağı durumlar olabilir. Bu tür durumlarda belirli bir boyutta içerisi null değerlerle dolu bir dizi oluşturup kullanabiliriz. Bu dizinin elemanlarını daha sonra istediğimiz gibi değiştirip kullanabiliriz. Bu tür bir dizi şu şekilde oluşturulabilir:

val nullValues= arrayOfNulls<String>(5)
println(nullValues.toList()) // output: [null, null, null, null, null]

Unit ( Hiçbir şey )

Unit veri tipi önceki gördüğümüz veri tiplerinden biraz farklıdır çünkü bir değer içermez. Bu veri tipi genelde fonksiyon dönüş türü olarak kullanılır ve kullanıldığı zaman fonksiyonun herhangi bir değer döndürmeyeceğini anlamamız gerekir. Fonksiyonlar konusundan sonraki yazılarımda bahsedeceğim şimdilik bu şekilde biliyor olmanız yeterli.

// Aşağıdaki fonksiyonu main fonksiyonda kullanırken herhangi bir değişkene atama yapmamız gerekmez çünkü herhangi bir değer döndürmez yalnızca içerisindeki println() işlemini gerçekleştirir.
fun sayHello() : Unit {
println("Merhaba, Dünya!")
}

Any ( Herhangi bir )

Any, Kotlin dilindeki en üst ve en genel veri tipidir. Bu tip bir değişken oluşturduğumuz zaman herhangi bir değer atayabiliriz ve herhangi bir nesne türünü temsil eder. Bu değişken tipini kullandığımız zaman hangi tip olarak kullandıysak o tipe özgün metotlara ulaşabilmemiz için tip kontrolü gibi ek işlemler yapmamız gerekir.

val anyObject: Any = "Merhaba, Dünya!" // Any tipine bir String atayalım

println(anyObject.length) // Bu satırda String özellikleri kullanmaya çalışırsak hata alırız.

if (anyObject is String) {
println(anyObject.length) // Ancak üst satırda değişkenimizin tipinin String olduğunu kontrol ettiğimiz için bu koşul içerisinde String metotlarına ulaşabiliriz.
}

Any tipini kullanmak bazı durumlarda işimize yarayabilir ancak çok gerekmedikçe bu veri tipi yerine hangi spesifik tip işimize yarayacaksa onu kullanmalıyız.

Nullables

Bir değişkenin nullable tanımlanması demek, o değişkenin null veya boş değer alabileceği anlamına gelmektedir ve tiplerin daha güvenli kullanılmasını sağlar.

Bir değişkeni nullable olarak tanımladığımız zaman hafızada o değişken “boxed” olarak işaretlenir ve “primitive” özelliğini kaybeder. Bu durumda değişken, referans tipli bir değişken olarak hafızaya kaydedileceği için “primitive” tiplere göre hafızada daha çok yer kaplar. Bu da geliştirici olarak bizim pek istemeyeceğimiz bir durum. Bundan dolayı değişkenleri nullable kullanmak her ne kadar tiplerin daha güvenli bir şekilde kullanılmasını sağlasa da dikkatli ve bilinçli kullanılması gerekir.

Değişken tipinin sonuna yazılan soru işareti( ? ) bizim bir değişkene null değer ataması yapabileceğimizi gösterir. Şu şekilde ifade edilir:

val nullableName: String? = "Esra"

Eğer biz değişkenimizde tip belirtmeden direkt olarak bir null değer ataması yaparsak o değişkenin tipi Kotlin’e özel bir tip olan Nothing? olarak kabul edilir. Bu veri tipini Any’nin tersi gibi düşünebiliriz. Hiçbir şey yok anlamına gelir.

// Bu iki satır aynı şeyi ifade eder.
val name = null
val name: Nothing? = null

Eğer biz nullable bir değişken ile bir işlem yapmak istersek. Gelen değerin null olup olmadığının kontrolünü sağlamalıyız. null değerlerinin kontrolünü yapmak, “NullPointerException” hatası almamızı önlemeye yöneliktir. Kontrol yapıları kullanarak null kontrolü yapabiliriz. Bir sonraki yazımda kontrol yapılarından bahsederken buraya değineceğim ama şimdilik bu yazımda operatör yardımı ile null kontrolü konusunu ele alalım.

Bunu yapmak için 3 tane yöntem sıralayabiliriz.

?. (Safe call operator) kullanımı: bir değişken null değilse o değeri istediğimiz işlemden geçirip sonucumuzu ekranda görebiliriz ama eğer değişkenimiz null ise ekrana güvenli olarak null değeri yazdırılır, uygulamamız çalışmaya devam eder.

nullableName?.uppercase() 
println(nullableName?.uppercase()) // output: ESRA

val nullableName2: String? = null
println(nullableName2?.uppercase()) // null

?: (Elvis operator) kullanımı: bir değişken null ise bu operatörden sonra yazılacak olan “default” değeri kullanmak için bu operatörü kullanmamız gerekir.

var name = nullableName2 ?: "Esra"
println(name) // nullableName2 null olduğu için output: Esra

!! (Not null assertion operator) kullanımı: bir değişkenin null değer olarak gelmeyeceğinden emin olduğumuz durumlarda bu operatörü kullanabiliriz. Ancak bu operatörü kullanmak tehlikelidir çünkü eğer biz bu değişkene null değeri gelmeyecek diye kesin bir şey belirttikten sonra değişken null değer alırsa “NullPointerException” hatası alırız.

println(nullableName!!.uppercase()) // output: Esra
println(nullableName2!!.uppercase()) // hata: NullPointerException

Vakit ayırıp buraya kadar okuduğunuz için teşekkür ederim. Yazımla ilgili tavsiyeleriniz veya sorularınız varsa benimle Linkedin üzerinden iletişime geçebilirsiniz. Bir sonraki yazımda görüşmek üzere. 👋🏼

--

--