JavaRush/Courses/Kotlin SELF/Tools for working with null: ?., ?:, !!

Tools for working with null: ?., ?:, !!

Open

1. Why you need ?. and ?:, and why a single if is not enough

When you first learn about null, the usual first reaction is: “OK, I’ll just write if (x != null) everywhere.” That’s a normal start, but pretty soon you’ll feel the pain: nested ifs grow, logic gets scattered across the code, and reading the program starts to feel like walking through a labyrinth (and at the end a minotaur may still be waiting).

Kotlin offers more expressive constructs: short operators that make null-handling part of the expression. This matters because many tasks are not “do it or don’t,” but “get a value if it exists, otherwise use a fallback.” That’s exactly what ?. and ?: are for.

To keep the picture in your head, here’s a mini table:

Operator How to read it Idea What it returns
?.
“if it’s not null — do it; otherwise return null” safe access most often T?
?:
“if the left side is null — take the right side” default value / fallback behavior often removes nullability
!!
“I swear this is not null” forced access T, but can crash

Next, we’ll go through each tool calmly and with examples.

2. Safe call ?.: safe access without crashes

People usually like ?. because it makes code shorter and safer. But the main point is that it makes the behavior explicit: right in the line of code you show that the value may be missing, and you’re prepared for that scenario. It’s like buckling a seat belt: you’re not planning an accident, but life feels calmer.

?. on properties: .length without risk

The simplest example is a string length. If the string is nullable, direct access is not allowed:

fun main() {
    val nickname: String? = null

    println(nickname.length) // not allowed: nickname may be null

    val len: Int? = nickname?.length
    println(len) // null
}

It’s important to notice one thing here: the result of nickname?.length has type Int?, because the operation might “not happen” (if the left side is null, there’s nothing to compute length for).

And that’s super logical: a safe call doesn’t “invent” a length out of thin air. It honestly says: “Either I return the length, or I return null.”

?. on methods: safe call uppercase()

Same idea with methods:

fun main() {
    val nickname: String? = "neo"

    val upper: String? = nickname?.uppercase()
    println(upper) // NEO
}

If nickname is null, then upper also becomes null, and the program won’t crash.

Safe call chains: “a ?. domino chain”

Usually ?. is used not once, but as a chain: “take a string → trim spaces → convert to uppercase → take length”. And at each step the value can become null (if the input was null).

fun main() {
    val raw: String? = "   Kotlin   "

    val len: Int? = raw
        ?.trim()
        ?.uppercase()
        ?.length

    println(len) // 6
}

It reads nicely: while the value exists — keep going; as soon as it doesn’t — the whole chain turns into null.

Mini diagram: what ?. does

To lock it in, let’s represent ?. as a small flowchart:

flowchart TD
    A["There is a value x: T?"] --> B{ x == null? }
    B -->|yes| C["Result = null"]
    B -->|no| D["Execute x.member / x.func()"]
    D --> E["Result = value"]

Yes, the operator looks like two characters, but it encodes a very specific logic.

3. The ?: operator

The ?: operator (Elvis, because it resembles Elvis Presley’s hairstyle) lets you replace null with a normal value (or with a normal action).

Elvis as a “default value”

The most common form:

fun main() {
    val nickname: String? = null

    val len: Int = nickname?.length ?: 0
    println(len) // 0
}

Pay attention to the type: nickname?.length returns Int?, but Elvis substitutes 0, so the final result is a plain Int.

This is one of the key patterns: ?. often “produces nullability,” and ?: often “removes nullability.”

Elvis and string messages: decent console UX

In console programs, it’s important not only “not to crash,” but also to talk to the user like a human.

fun main() {
    val nickname: String? = null

    val label: String = nickname?.uppercase() ?: "ANONYMOUS"
    println("Hello, $label!") // Hello, ANONYMOUS!
}

If there is a value — we format it nicely. If not — we show a fallback text.

Elvis as an early exit

Sometimes null is not “let’s plug in 0,” but “we can’t proceed.” Then instead of a default value, we exit the function early.

Kotlin allows using return together with Elvis:

fun parsePositiveInt(text: String): Int? {
    val x: Int = text.toIntOrNull() ?: return null
    if (x <= 0) return null
    return x
}

fun main() {
    println(parsePositiveInt("42"))   // 42
    println(parsePositiveInt("oops")) // null
    println(parsePositiveInt("-5"))   // null
}

The idea is: we “localize the problem” immediately. If parsing failed — return null and don’t build nested ifs.

4. Trust me, bro

The !! operator looks like a simple way to “shut the compiler up”: it turns T? into T. But it does it very bluntly: if the value is actually null, the program crashes at runtime (usually with KotlinNullPointerException).

Key idea: !! does not make your code safer. It makes your code compilable, and shifts safety onto your certainty.

Example: compiles, but crashes

fun main() {
    val nickname: String? = null

    val len: Int = nickname!!.length
    println(len) // the program won't reach here
}

If you run this code, you’ll get a crash. In other words, !! is not “handling null”; it’s “I guarantee it’s not null.”

When !! can be appropriate at all

No fanaticism: you can think of !! as an emergency “break glass in case of fire” button. Sometimes it’s appropriate when you are absolutely sure the value cannot be null, and if it suddenly is null — it’s truly a bug you want to detect immediately.

But in practice, when a beginner uses !!, it often means: “I didn’t understand how to handle null properly.” So the basic rule is simple: first look for a solution using ?., ?:, or an if check, and only then (in rare cases) consider !!.

4. User questionnaire: optional fields and handling null

Let’s continue evolving the same console app: a simple “user questionnaire.” Earlier we read strings, did trim(), parsed numbers via toIntOrNull(). Now we’ll add optional fields (which can be skipped), and apply ?., ?: and some neat patterns.

Agree on semantics: empty string vs null

A very common confusion: the user pressed Enter and entered an empty string "". That is not null. But for our logic, “entered nothing” often means “there is no value.” So we can convert an empty string into null ourselves.

Let’s make a small helper function:

fun emptyToNull(text: String): String? {
    val trimmed = text.trim()
    return if (trimmed.isEmpty()) null else trimmed
}

Read a nickname as String? and format the output

fun emptyToNull(text: String): String? {
    val trimmed = text.trim()
    return if (trimmed.isEmpty()) null else trimmed
}

fun main() {
    println("Enter a nickname (can be empty):")
    val nicknameInput = readln()
    val nickname: String? = emptyToNull(nicknameInput)

    val label = nickname?.uppercase() ?: "ANONYMOUS"
    println("Your label: $label") // if empty: "Your label: ANONYMOUS"
}

Here we use two tools at once: ?.uppercase() and ?: "ANONYMOUS".

Age: toIntOrNull() returns Int?, and that’s fine

Now let’s ask for age, but make it optional (for example, the user doesn’t want to share it).

fun emptyToNull(text: String): String? {
    val trimmed = text.trim()
    return if (trimmed.isEmpty()) null else trimmed
}

fun main() {
    println("Enter age (can be empty):")
    val ageText: String? = emptyToNull(readln())

    val age: Int? = ageText?.toIntOrNull()
    println("age = $age") // for example: age = 25 or age = null
}

A very important point: we cannot write ageText.toIntOrNull(), because ageText is nullable. So it’s either if or ?.. We chose ?..

Nice output: build a phrase, not “null”

Now let’s produce a normal message for the user:

fun emptyToNull(text: String): String? {
    val trimmed = text.trim()
    return if (trimmed.isEmpty()) null else trimmed
}

fun main() {
    println("Enter age (can be empty):")
    val ageText: String? = emptyToNull(readln())

    val age: Int? = ageText?.toIntOrNull()

    val ageLabel: String = age?.toString() ?: "not specified"
    println("Age: $ageLabel") // for example: "Age: not specified"
}

Here we made one more “bridge”: age is Int?, and Elvis turns it into text that you can print without embarrassment.

Pattern “handle null immediately”: avoid nesting

Imagine we want to estimate “birth year approximately” (roughly 2026 - age). Important: 2026 here is just an example year. If there is no age, computing it is pointless.

Let’s make a function that returns a result string:

fun estimateBirthYear(ageText: String?): String {
    val age: Int = ageText?.toIntOrNull() ?: return "Birth year: unknown"
    val year = 2026 - age
    return "Approximate birth year: $year"
}

fun main() {
    println(estimateBirthYear("20"))   // Approximate birth year: 2006
    println(estimateBirthYear(null))   // Birth year: unknown
    println(estimateBirthYear("abc"))  // Birth year: unknown
}

Here Elvis is used not with a default value, but with a default behavior: early return.

Putting the whole questionnaire together

Let’s assemble a simplified version: name is required; nickname and age are optional.

fun emptyToNull(text: String): String? {
    val trimmed = text.trim()
    return if (trimmed.isEmpty()) null else trimmed
}

fun main() {
    println("Name (required):")
    val name: String = readln().trim()

    println("Nickname (can be empty):")
    val nickname: String? = emptyToNull(readln())

    println("Age (can be empty):")
    val ageText: String? = emptyToNull(readln())
    val age: Int? = ageText?.toIntOrNull()

    val shownNickname = nickname?.uppercase() ?: "ANONYMOUS"
    val shownAge = age?.toString() ?: "not specified"

    println("Profile: $name / $shownNickname / age: $shownAge")
    // Example: "Profile: Alice / ANONYMOUS / age: not specified"
}

Here we have a neat “processing pipeline”:

readln() → trim() → (maybe) emptyToNull() → (maybe) ?.toIntOrNull() → formatting via ?:.

That’s a healthy style of working with nullable values: no overkill, but no denial either.

5. Common mistakes when working with ?., ?:, and !!

Mistake No. 1: expecting ?. to substitute a default value.
Beginners sometimes read a safe call as: “if it’s null, it will somehow fix itself.” No: ?. only guarantees the program won’t crash when accessing a property/method. If the left side is null, the expression result will also be null. If you need a default, that’s the job of Elvis ?:, and the default should be meaningful, not “just to make it compile.”

Mistake No. 2: a ?. chain becomes long and stops being readable.
Technically, you can write five or six safe calls in a row, and it will even work. But the human brain is not a compiler: it’s hard to keep long chains in mind, especially when there’s also ?: at the end. In such cases, it’s better to create intermediate vals and give them clear names, so the code reads like steps, not like a spell.

Mistake No. 3: using ?: 0 or ?: "" without meaning, masking the real problem.
Elvis is very tempting: add ?: 0 — and everything “works.” But sometimes it hides an input error or breaks logic. For example, age 0 may be a valid number in mathematics, but for a questionnaire it’s probably weird. Prefer defaults that make sense for your domain: “not specified,” “unknown,” “—,” or an early return if further logic is pointless without the value.

Mistake No. 4: fixing every compiler error with !!.
!! really does “fix” the compiler message quickly, because it converts T? to T. But the cost is a runtime crash risk, often in a place you don’t expect (especially if the value came from input). At your stage, it’s much more correct to treat !! as a rare tool for cases where crashing on null is a conscious contract, not a universal hammer for all nails.

Mistake No. 5: mixing up null and an empty string, and therefore combining ?. and ?: in a weird way.
If you don’t agree on what “the user entered nothing” means, you’ll get surprises. An empty string "" is a value, and ?. does not apply to it at all (it’s not null). So it’s often useful to normalize input explicitly, converting “empty after trim()” to null. Then ?. and ?: start working exactly as you expect, and the program becomes predictable.

1
Task
Kotlin SELF,  11 level,  3 lecture
Locked
Greeting label
Greeting label
1
Task
Kotlin SELF,  11 level,  3 lecture
Locked
Meaningful length
Meaningful length
1
Task
Kotlin SELF,  11 level,  3 lecture
Locked
Task report
Task report
1
Task
Kotlin SELF,  11 level,  3 lecture
Locked
Access code
Access code
Комментарии
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны sign in
This page has no comments yet