Оператор Элвиса в Kotlin: ?: работает не так, как ты думал

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Оператор Элвиса в Kotlin: не просто "если null, то…"

Если вы когда-нибудь писали на Kotlin и сталкивались с null, то наверняка видели этот забавный смайлик с прической — оператор ?:. Его называют Elvis Operator, и в сообществе Kotlin он стал настоящим мемом. Но за этим простым синтаксисом скрывается неожиданно сложная и интересная механика.

Сегодня мы разберём оператор Элвиса в Kotlin не как очередной "синтаксический сахар", а как мощный инструмент, который на уровне байткода решает проблему null-безопасности. Вы узнаете, что происходит, когда компилятор встречает ?:, и почему в некоторых случаях он работает быстрее, чем if-else.

Статья основана на реальном вопросе с Stack Overflow с тысячами просмотров — мы адаптировали его для глубокого образовательного контента. Поехали!

Что такое Elvis Operator и с чем его едят

Формально оператор Элвиса (Elvis Operator) — это бинарный оператор, который возвращает левый операнд, если он не равен null, и правый — в противном случае. Синтаксис:

val result = nullableValue ?: defaultValue

Название пошло от сходства с причёской Элвиса Пресли, если повернуть голову набок: ?: — это глаза и чубчик. Забавно, но в мире программирования это имя прижилось.

В Kotlin этот оператор — часть системы null safety. Он позволяет писать лаконичный код без громоздких проверок:

// Вместо этого:
val length: Int
if (str != null) {
    length = str.length
} else {
    length = 0
}

// Пишем так:
val length = str?.length ?: 0

Подвох, о котором молчат в туториалах

Многие думают, что ?: — это просто сокращённая запись if-else. Но это не совсем так. Компилятор Kotlin генерирует разный байткод для этих двух конструкций. И разница может повлиять на производительность и поведение программы.

Как работает ?: под капотом: байткод и JVM

Давайте заглянем под капот. Возьмём простой пример и посмотрим, во что его превращает компилятор Kotlin для JVM.

Исходный код на Kotlin

fun getLength(str: String?): Int {
    return str?.length ?: 0
}

Декомпилированный Java-код (примерно)

public static final int getLength(@Nullable String str) {
    Integer var10000 = str != null ? str.length() : null;
    return var10000 != null ? var10000 : 0;
}

Видите? Компилятор не просто заменяет ?: на if-else. Он создаёт временную переменную типа Integer? (боксинг!), а потом применяет второй ?:. Это называется двойная проверка.

Почему так происходит?

Потому что str?.length возвращает Int? (nullable Int). А ?: ожидает, что левый операнд может быть null. Компилятор не может "склеить" эти две проверки в одну — он обязан сохранить семантику безопасного вызова.

В результате, если str не null, выполняются две проверки на null вместо одной. Для большинства приложений это незаметно, но в высоконагруженных циклах — может стать узким местом.

Оптимизация: когда Elvis Operator быстрее if-else

Но есть и хорошая новость. В некоторых случаях Elvis Operator генерирует более эффективный байткод, чем ручная проверка.

Рассмотрим пример с ранним возвратом:

// С if-else
fun process(value: String?): String {
    if (value == null) return "default"
    return value.uppercase()
}

// С Elvis
fun process(value: String?): String {
    return value?.uppercase() ?: "default"
}

Декомпиляция покажет, что вариант с ?: использует таблицу переходов (tableswitch) в байткоде, а не условные переходы (if_icmpne). Это позволяет JIT-компилятору лучше оптимизировать код на горячих участках.

Вывод: Elvis Operator не просто красивее — он может быть быстрее, если используется в цепочках вызовов.

Реальные примеры из практики

Давайте посмотрим, как оператор ?: в Kotlin применяется в реальных проектах.

1. Значения по умолчанию

Самый частый сценарий — подстановка дефолтного значения:

val userName = user?.name ?: "Гость"
val discount = order?.discount ?: 0.0
val config = settings?.theme ?: Theme.DARK

2. Цепочки вызовов (safe call chain)

val city = user?.address?.city ?: "Неизвестно"

Здесь если любой из элементов цепочки — null, результатом будет "Неизвестно". Без Элвиса пришлось бы писать вложенные if.

3. Выброс исключения с Elvis

Мало кто знает, но справа от ?: можно использовать throw или return:

val id = requireNotNull(inputId) ?: throw IllegalArgumentException("ID не может быть null")
// или короче:
val id = inputId ?: error("ID не может быть null")

Это эквивалентно requireNotNull или checkNotNull, но выглядит выразительнее.

4. Elvis в коллекциях

val firstElement = list?.firstOrNull() ?: "Список пуст"
val mapValue = map["key"] ?: "Ключ отсутствует"

Подводные камни и частые ошибки

Опытные разработчики знают: Elvis Operator коварен. Вот три ситуации, где он может вас подвести.

Ошибка 1: Elvis с пустыми строками

val name = "".trim() ?: "Default" // Результат: "" (пустая строка), не "Default"!

Оператор ?: проверяет только null, а не пустоту. Пустая строка — это не null. Если нужно проверять и пустоту, используйте ifBlank

Блоги

Рекомендаций по книгам