JavaRush /Курсы /Kotlin SELF /Инструменты работы с null: ?., ?:, !!

Инструменты работы с null: ?., ?:, !!

Kotlin SELF
11 уровень , 3 лекция
Открыта

1. Зачем нужны ?. и ?: и почему одного if мало

Когда вы только узнали про null, первая реакция обычно такая: «Окей, буду везде писать if (x != null)». Это нормальный старт, но довольно быстро вы почувствуете боль: вложенные if разрастаются, логика расползается по коду, а чтение программы становится похоже на прохождение лабиринта (и в конце вас всё равно может ждать минотавр).

Kotlin предлагает более выразительные конструкции: короткие операторы, которые делают обработку null частью выражения. Это важно, потому что многие задачи — это не «сделай действие или не делай», а «получи значение, если оно есть, иначе используй запасной вариант». Именно для этого существуют ?. и ?:.

Чтобы держать картинку в голове, вот мини-таблица:

Оператор Как читается Идея Что возвращает
?.
«если не null — сделай, иначе верни null» безопасный доступ чаще всего T?
?:
«если слева null — возьми справа» значение по умолчанию / запасное поведение часто снимает nullable
!!
«я клянусь, что тут не null» принудительный доступ T, но риск падения

Дальше разберём каждый инструмент спокойно и на примерах.

2. Safe call ?.: безопасный доступ без падений

Оператор ?. обычно любят за то, что он делает код короче и безопаснее. Но главное — он делает поведение явным: вы прямо в строке кода показываете, что значение может отсутствовать, и вы готовы к такому сценарию. Это как пристегнуться ремнём безопасности: вы не планируете аварию, но жить становится спокойнее.

?. на свойствах: .length без риска

Самый простой пример — длина строки. Если строка nullable, то прямой доступ запрещён:

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

    println(nickname.length) // так нельзя: nickname может быть null

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

Здесь важно заметить одну вещь: результат nickname?.length имеет тип Int?, потому что операция могла «не выполниться» (если слева null, длину вычислять нечему).

И это супер-логично: safe call не «придумывает» длину из воздуха. Он честно говорит: «Либо я верну длину, либо null».

?. на методах: безопасный вызов uppercase()

Тот же принцип с методами:

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

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

Если nickname будет null, то upper тоже станет null, и программа не упадёт.

Цепочки safe call: «домино из ?.»

Обычно ?. используют не один раз, а цепочкой: «возьми строку → обрежь пробелы → приведи к верхнему регистру → возьми длину». И на каждом шаге значение может стать null (если на входе было null).

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

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

    println(len) // 6
}

Читается приятно: пока значение есть — идём дальше; как только значения нет — вся цепочка превращается в null.

Мини-схема: что делает ?.

Чтобы закрепить, представим работу ?. как маленькую блок-схему:

flowchart TD
    A["Есть значение x: T?"] --> B{ x == null? }
    B -->|да| C["Результат = null"]
    B -->|нет| D["Выполняем x.member / x.func()"]
    D --> E["Результат = значение"]

Да, оператор выглядит как два символа, но за ним стоит очень конкретная логика.

3. Оператор ?:

Оператор ?: (Elvis, т.к. напоминает прическу Элвиса Пресли) позволяет заменить null на нормальное значение (или на нормальное действие).

Elvis как «дефолтное значение»

Самая частая форма:

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

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

Обратите внимание на тип: слева nickname?.length даёт Int?, но Elvis подставляет 0, поэтому итог — обычный Int.

Это один из ключевых паттернов: ?. часто «порождает nullable», а ?: часто «снимает nullable».

Elvis и строковые сообщения: нормальный UX в консоли

В наших консольных программах важно не только «не упасть», но и говорить с пользователем по-человечески.

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

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

Если значение есть — красиво форматируем. Если нет — показываем запасной текст.

Elvis как ранний выход

Иногда null — это не «давай подставим 0», а «значит, дальше работать нельзя». Тогда вместо дефолта мы делаем ранний выход из функции.

Kotlin позволяет использовать return вместе с 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
}

Идея такая: мы «локализуем проблему» сразу. Если не распарсилось — возвращаем null и не строим вложенные if.

4. Поверь мне, брат

Оператор !! выглядит как простой способ «заткнуть компилятор»: он превращает T? в T. Но делает это очень прямолинейно: если значение оказалось null, программа падает во время выполнения (обычно это KotlinNullPointerException).

Важная мысль: !! не делает ваш код безопаснее. Он делает ваш код компилируемым, а безопасность перекладывает на вашу уверенность.

Пример: компилируется, но падает

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

    val len: Int = nickname!!.length
    println(len) // сюда программа не дойдёт
}

Если запустить этот код, вы получите падение программы. То есть !! — это не «обработка null», это «я гарантирую не null».

Когда !! вообще бывает уместен

Без фанатизма: !! можно рассматривать как аварийную кнопку «разбить в случае пожара». Иногда он уместен, когда вы абсолютно уверены, что значения не может быть null, и если оно вдруг null — это действительно баг, который лучше обнаружить сразу.

Но на практике, когда новичок ставит !!, это часто означает: «Я не понял, как правильно обработать null». Поэтому базовое правило простое: сначала ищем решение через ?., ?: или проверку if, и только потом (в редких случаях) рассматриваем !!.

4. Анкета пользователя: опциональные поля и обработка null

Давайте продолжим развивать одно и то же консольное приложение: простую «анкету пользователя». Раньше мы читали строки, делали trim(), парсили числа через toIntOrNull(). Теперь добавим опциональные поля (которые могут быть пропущены), и применим ?., ?: и аккуратные паттерны.

Договоримся о смысле: пустая строка vs null

Очень частая путаница: пользователь нажал Enter и ввёл пустую строку "". Это не null. Но для нашей логики «ничего не ввёл» часто означает «значения нет». Поэтому мы можем сами превратить пустую строку в null.

Сделаем маленькую функцию-помощник:

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

Читаем никнейм как String? и форматируем вывод

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

fun main() {
    println("Введите никнейм (можно пусто):")
    val nicknameInput = readln()
    val nickname: String? = emptyToNull(nicknameInput)

    val label = nickname?.uppercase() ?: "ANONYMOUS"
    println("Ваш ярлык: $label") // если пусто: "Ваш ярлык: ANONYMOUS"
}

Здесь сразу два инструмента: ?.uppercase() и ?: "ANONYMOUS".

Возраст: toIntOrNull() даёт Int?, и это нормально

Теперь спросим возраст, но сделаем его необязательным (например, пользователь не хочет делиться).

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

fun main() {
    println("Введите возраст (можно пусто):")
    val ageText: String? = emptyToNull(readln())

    val age: Int? = ageText?.toIntOrNull()
    println("age = $age") // например: age = 25 или age = null
}

Очень важный момент: мы не можем сделать ageText.toIntOrNull(), потому что ageText nullable. Поэтому либо if, либо ?.. Мы выбрали ?..

Красивый вывод: формируем фразу, а не «null»

Теперь сделаем нормальное сообщение для пользователя:

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

fun main() {
    println("Введите возраст (можно пусто):")
    val ageText: String? = emptyToNull(readln())

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

    val ageLabel: String = age?.toString() ?: "не указан"
    println("Возраст: $ageLabel") // например: "Возраст: не указан"
}

Тут мы сделали ещё один «мостик»: ageInt?, а Elvis превращает это в текст, который можно вывести без стыда.

Паттерн «обработай null сразу»: избегаем вложенности

Представим, что мы хотим посчитать «год рождения приблизительно» (условно 2026 - age). Важно: число 2026 здесь просто выбранный год для примера. Если возраста нет, считать бессмысленно.

Сделаем функцию, которая возвращает строку-результат:

fun estimateBirthYear(ageText: String?): String {
    val age: Int = ageText?.toIntOrNull() ?: return "Год рождения: неизвестен"
    val year = 2026 - age
    return "Год рождения примерно: $year"
}

fun main() {
    println(estimateBirthYear("20"))   // Год рождения примерно: 2006
    println(estimateBirthYear(null))   // Год рождения: неизвестен
    println(estimateBirthYear("abc"))  // Год рождения: неизвестен
}

Здесь Elvis используется не с дефолтным значением, а с дефолтным поведением: ранний возврат.

Сборка анкеты целиком

Соберём упрощённую версию анкеты: имя обязательно, никнейм и возраст — нет.

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

fun main() {
    println("Имя (обязательно):")
    val name: String = readln().trim()

    println("Никнейм (можно пусто):")
    val nickname: String? = emptyToNull(readln())

    println("Возраст (можно пусто):")
    val ageText: String? = emptyToNull(readln())
    val age: Int? = ageText?.toIntOrNull()

    val shownNickname = nickname?.uppercase() ?: "ANONYMOUS"
    val shownAge = age?.toString() ?: "не указан"

    println("Профиль: $name / $shownNickname / возраст: $shownAge")
    // Пример: "Профиль: Alice / ANONYMOUS / возраст: не указан"
}

Здесь у нас аккуратная «труба обработки»:

readln()trim() → (возможно) emptyToNull() → (возможно) ?.toIntOrNull() → форматирование через ?:.

Это и есть нормальный стиль работы с nullable: без перегибов, но и без игнорирования.

5. Типичные ошибки при работе с ?., ?: и !!

Ошибка №1: ожидать, что ?. подставляет значение по умолчанию.
Новички иногда читают safe call так: «если null, то как-нибудь само починится». Нет: ?. всего лишь гарантирует, что программа не упадёт на обращении к свойству/методу. Если слева null, результат выражения тоже будет null. Если вам нужен дефолт, это уже задача Elvis ?:, и дефолт должен быть осмысленным, а не «лишь бы компилировалось».

Ошибка №2: цепочка ?. становится длинной и перестаёт читаться.
Технически можно написать пять-шесть safe call подряд, и оно даже будет работать. Но мозг человека не компилятор: ему сложно держать в голове длинные цепочки, особенно если там ещё и ?: в конце. В таких случаях лучше сделать промежуточные val и дать им понятные имена, чтобы код читался как шаги, а не как заклинание.

Ошибка №3: использовать ?: 0 или ?: "" без смысла и тем самым маскировать проблему.
Elvis очень соблазнителен: поставил ?: 0 — и всё «работает». Но иногда это скрывает ошибку ввода или ломает логику. Например, возраст 0 может быть валидным числом в математике, но для анкеты это скорее странность. Лучше выбирать такие дефолты, которые имеют смысл для вашей предметной области: «не указан», «неизвестно», «—», либо ранний return, если без значения дальнейшая логика бессмысленна.

Ошибка №4: лечить все ошибки компилятора через !!.
!! действительно быстро «чинит» сообщение компилятора, потому что превращает T? в T. Но цена этого — риск падения в рантайме, причём часто в месте, где вы этого не ожидаете (особенно если значение пришло из ввода). На вашем этапе гораздо правильнее воспринимать !! как редкий инструмент для ситуаций, где падение при null — осознанный контракт, а не как универсальный молоток для всех гвоздей.

Ошибка №5: путать null и пустую строку и из-за этого странно комбинировать ?. и ?:.
Если вы не договорились, что означает «пользователь ничего не ввёл», вы будете получать сюрпризы. Пустая строка "" — это значение, и ?. к нему вообще не относится (она не null). Поэтому часто полезно явно нормализовать ввод, превращая «пусто после trim()» в null. Тогда ?. и ?: начинают работать ровно так, как вы ожидаете, и программа становится предсказуемой.

1
Задача
Kotlin SELF, 11 уровень, 3 лекция
Недоступна
Приветственный ярлык
Приветственный ярлык
1
Задача
Kotlin SELF, 11 уровень, 3 лекция
Недоступна
Полезная длина
Полезная длина
1
Задача
Kotlin SELF, 11 уровень, 3 лекция
Недоступна
Отчёт по задачам
Отчёт по задачам
1
Задача
Kotlin SELF, 11 уровень, 3 лекция
Недоступна
Код доступа
Код доступа
Комментарии (3)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Khagnar Уровень 62
13 марта 2026
"Иногда он уместен, когда вы абсолютно уверены, что значения не может быть null, и если оно вдруг null — это действительно баг, который лучше обнаружить сразу." я конечно не знаю как в котлине делают, но в java если значение не может быть null по бизнес логике, а оно внезапно null, то например используют конструкцию

if (value == null) {
    throw new RuntimeException("value cannot be null")
}
которая как будто бы информативней, чем просто рандомный NPE в логах
kasnil Уровень 66
20 марта 2026
Через оператор ?: (Elvis operator):

value ?: RuntimeException("value cannot be null")
Серега PARADOX Уровень 21
29 марта 2026
В котлине, получается, уходим от отдельных ифов на Null, а кидаем исключения сразу в момент присваивания. А если простыня параметров то в Nullable{} блоке