null și null safety (?. ?: !!) în Kotlin

null înseamnă „nicio valoare”. Kotlin separă tipurile care acceptă null (String?) și oferă ?., ?: și !! pentru ele.

Sintaxă
var nume: String? = null    // tip care acceptă null
nume?.length                // null dacă nume e null
nume?.length ?: 0           // valoare de rezervă
nume!!.length               // NullPointerException dacă e null
nume?.let { ... }           // doar dacă nu e null

Ce face null și null safety (?. ?: !!)

null înseamnă că o variabilă nu are nicio valoare. În Kotlin, un tip obișnuit (String, Int) nu poate primi niciodată null; compilatorul nu te lasă. Ca să accepte null, tipul are un semn de întrebare: String?. Pe o valoare String? nu poți apela direct .length: întâi trebuie să tratezi cazul null. Așa dispar cele mai multe erori NullPointerException din Java.

Operatorii pentru null: ?. (apel sigur) apelează metoda doar dacă valoarea nu e null, altfel dă null; ?: (elvis) dă o valoare de rezervă când stânga e null; !! spune „sunt sigur că nu e null” și aruncă NullPointerException dacă te-ai înșelat. După un if (x != null), compilatorul tratează x ca nenul (smart cast).

Multe funcții din biblioteca standard întorc tipuri ? exact ca să te oblige să tratezi lipsa valorii: toIntOrNull(), firstOrNull(), map[cheie], readlnOrNull(). Combinația x?.let { ... } execută un bloc doar când valoarea există.

Exemple

Exemplul 1: ?. și ?: pe valori care pot lipsi

map[cheie] întoarce String?, pentru că orașul poate lipsi. ?. apelează uppercase doar dacă există, iar ?: dă textul de rezervă.

fun main() {
    val orase: Map<String, String> = mapOf("Ana" to "Iași", "Mihai" to "Brașov")
    for (nume in listOf("Ana", "Ioana")) {
        val oras: String? = orase[nume]
        println("$nume: ${oras?.uppercase() ?: "oraș necunoscut"}")
        println("  lungime: ${oras?.length}")
    }
}

Rezultat

Ana: IAȘI
  lungime: 4
Ioana: oraș necunoscut
  lungime: null

Exemplul 2: if (x != null), let și filterNotNull

După verificarea cu if, nota este folosită ca Int. let rulează doar pentru valorile nenule.

fun main() {
    val raspunsuri = listOf("9", "zece", "7", "")
    for (text in raspunsuri) {
        val nota: Int? = text.toIntOrNull()
        if (nota != null) {
            println("'$text' -> nota $nota, dublată ${nota * 2}")
        } else {
            println("'$text' nu este o notă")
        }
    }
    raspunsuri.firstOrNull { it.startsWith("z") }?.let { println("Primul text cu z: $it") }
    val valide: List<Int> = raspunsuri.map { it.toIntOrNull() }.filterNotNull()
    println("Media notelor valide: ${valide.average()}")
}

Rezultat

'9' -> nota 9, dublată 18
'zece' nu este o notă
'7' -> nota 7, dublată 14
'' nu este o notă
Primul text cu z: zece
Media notelor valide: 8.0

Exemplul 3: !! și NullPointerException

!! nu verifică nimic: când valoarea este null, aruncă NullPointerException. Aici excepția este prinsă ca să vezi ce se întâmplă.

fun gasesteTelefon(nume: String): String? =
    mapOf("Ana" to "0722 111 222")[nume]

fun main() {
    println("Ana: ${gasesteTelefon("Ana")!!}")
    try {
        println("Dan: ${gasesteTelefon("Dan")!!}")
    } catch (e: NullPointerException) {
        println("Eroare: ${e::class.simpleName}")
    }
    println("Dan, în siguranță: ${gasesteTelefon("Dan") ?: "fără telefon"}")
}

Rezultat

Ana: 0722 111 222
Eroare: NullPointerException
Dan, în siguranță: fără telefon

Greșeli frecvente

  • Folosirea lui !! peste tot ca să „scapi” de erorile de compilare: aduce înapoi NullPointerException. Preferă ?., ?: sau un if.
  • ?. sau ?: pe un tip care nu poate fi null: compilatorul avertizează (unnecessary safe call), iar cu -Werror compilarea eșuează.
  • Lanțul a?.b.c: dacă a e null, a?.b e null și apoi .c nu compilează. Scrie a?.b?.c.

Comenzi înrudite

  • is, !is și smart cast: is verifică tipul unei valori; după verificare, compilatorul o tratează automat ca acel tip (smart cast).
  • as și as? (conversie de tip): as tratează o valoare ca alt tip (aruncă excepție dacă nu se potrivește); as? întoarce null în loc de excepție.
  • lateinit: lateinit permite unei proprietăți var ne-nullable să primească valoare mai târziu, după crearea obiectului.
  • return: Încheie funcția curentă și întoarce o valoare apelantului; cu etichetă (return@forEach) iese doar din lambda.
  • true și false (Boolean): true și false sunt cele două valori ale tipului Boolean, folosite în condiții, comparații și operatori logici.