Многие языки, включая Java, позволяют присвоить переменной специальное значение null, означающее отсутствие ссылки. Это удобно, но чревато ошибками: если попытаться вызвать метод у null, программа упадёт с NullPointerException (NPE).
В Java защита от NPE требует ручных проверок:
// Java String str = getString(); // может вернуть null int length; if (str != null) { length = str.length(); } else { length = 0; }
А что, если мы забудем проверку? Приложение упадёт. Kotlin подходит к проблеме иначе: язык встраивает null‑безопасность в систему типов. Переменная по умолчанию не может быть null, а если она может, вы обязаны явно это указать и обработать возможность отсутствия значения.
Пример на Kotlin:
// Kotlin val str: String? = getString() // тип String? означает "может быть null" val length = str?.length ?: 0 // безопасный вызов + значение по умолчанию
Компилятор не позволит просто написать str.length - он заставит вас принять решение. Именно эта строгость делает Kotlin‑код гораздо надёжнее.
Важно понимать: null из языка никуда не исчез. Он остаётся, но становится управляемым. Мы сами решаем, где null допустим, и всегда видим это по типу.
Если вы только начинаете знакомство с Kotlin, рекомендую прочитать нашу статью «Kotlin или Java в 2026: что выбрать» - там подробно разобраны преимущества языка.
В Kotlin каждый тип имеет две версии: обычную (non‑null) и nullable (с вопросительным знаком).
// Non‑null тип – нельзя присвоить null var a: String = "Привет" // a = null // Ошибка компиляции! // Nullable тип – можно присвоить null var b: String? = "Привет" b = null // ОК
Если переменная объявлена как nullable, вызывать её методы напрямую нельзя – компилятор выдаст ошибку:
val len = b.length // Ошибка: только safe (?.) или non‑null asserted (!!) вызовы разрешены
Это заставляет разработчика явно решить, что делать в случае null.
Распространённая ошибка – забыть добавить вопросительный знак и потом удивляться, почему нельзя присвоить null. Всегда проверяйте, действительно ли переменная должна допускать отсутствие значения.
Есть два основных способа проверить значение на null: классический if и оператор безопасного вызова ?..
После явной проверки if (x != null) компилятор автоматически приводит тип переменной к non‑null в области видимости условия:
val str: String? = "Hello" if (str != null) { // здесь str автоматически стал типом String (smart cast) println(str.length) // работает без ?. }
Однако smart‑cast работает только для локальных переменных val, которые не могли измениться после проверки. Для var или полей класса используйте локальную копию.
Если нужно просто вызвать метод или получить свойство, удобнее ?. – он выполняет вызов только если объект не null, иначе возвращает null.
val str: String? = getString() val length = str?.length // тип Int? (nullable)
Можно строить цепочки вызовов:
val cityName = user?.address?.city?.name
Если хотя бы один из элементов цепочки окажется null, результатом будет null.
| Ситуация | Подход |
|---|---|
| Простое получение значения (метод/свойство) | ?. |
| Несколько операций над одним объектом | if или ?.let |
| Работа с var‑переменными | локальная копия + if или ?. |
| Сложная логика, требующая нескольких проверок | if (структурно) или комбинация ?.let |
Ошибка: полагаться на smart‑cast для var (он может подвести). Лучше явно сохранить значение в локальную переменную.
Оператор ?: (называемый «Элвис» из‑за сходства с причёской певца) позволяет задать значение по умолчанию, если выражение слева равно null.
val str: String? = null val length = str?.length ?: 0 // если str?.length вернул null, используем 0
Справа может быть выражение любого типа, совместимого с ожидаемым.
Элвис часто используют для досрочного выхода. Возможны два синтаксически правильных варианта:
// Вариант 1: возвращаем значение по умолчанию (функция возвращает String) fun getUserName(user: User?): String { return user?.name ?: "Unknown" } // Вариант 2: ранний выход из функции, возвращающей Unit (void) fun processUser(user: User?) { user?.name ?: return // если name == null, выходим // здесь мы знаем, что user.name != null, но компилятор не запоминает это // поэтому лучше сохранить в локальную переменную: val name = user.name ?: return println(name) }
run для логированияfun process() { val data = fetchData() ?: run { logError("No data") return // возврат из внешней функции process() } // работаем с data }
Здесь return внутри лямбды run завершает внешнюю функцию process(). Это допустимо и часто используется.
val text = intent.getStringExtra("TEXT") ?: "" // text никогда не будет null (пустая строка, если extra отсутствует)
Ошибка: использовать ?: после выражения, которое заведомо не может быть null (бессмысленно). Или забывать, что справа может быть любой код, включая return или throw.
Kotlin создан для совместной работы с Java. При вызове Java‑метода Kotlin не всегда знает, может ли метод вернуть null. В этом случае возникает платформенный тип (обозначается как String!). Вы можете обращаться с ним как с nullable или как с non‑null – ответственность ложится на вас.
val view = findViewById(R.id.my_view) // тип View! (платформенный)re>
На самом деле findViewById может вернуть null, если view нет, поэтому лучше обрабатывать его как nullable:
val view = findViewById(R.id.my_view) ?: return
Примечание: в современных проектах рекомендуется использовать View Binding вместо ручного поиска findViewById. Это автоматически даёт безопасные типы.
Многие Android‑методы снабжены аннотациями @Nullable и @NonNull. Kotlin их учитывает: если метод помечен @Nullable, тип станет nullable; если @NonNull – non‑null.
// Аннотированный метод Java @Nullable String getName(); // в Kotlin будет String? @NonNull String getTitle(); // в Kotlin будет String
Проблема возникает, когда аннотаций нет. Например, cursor.getString(columnIndex) из старого Java‑кода может вернуть null, но тип будет платформенным.
Лучшая практика: всегда явно обрабатывать результат Java‑методов как nullable, если нет уверенности. Используйте ?., ?: или проверку через if.
// Неправильно: предполагаем, что имя точно есть val name = cursor.getString(0) // тип String! println(name.length) // может упасть NPE // Правильно: безопасно val name = cursor.getString(0) ?: "" println(name.length)
Ошибка: считать, что любой Java‑метод возвращает non‑null. Это частая причина NPE в смешанных проектах.
В Android часто нужно объявить поле, которое будет инициализировано позже (например, адаптер или TextView). Для этого Kotlin предлагает модификатор lateinit (работает только с var, не с val, и только с не-примитивными типами):
lateinit var textView: TextView lateinit var adapter: MyAdapter // пользовательский класс // где‑то позже textView = findViewById(R.id.my_text) adapter = MyAdapter() // перед использованием можно проверить, инициализировано ли поле if (::textView.isInitialized) { textView.text = "Hello" }
Обращение к неинициализированному lateinit полю вызовет исключение. Используйте эту возможность осознанно.
Часто коллекции содержат nullable элементы. Например, список email‑адресов, где некоторые могут отсутствовать. Kotlin предоставляет удобные функции для работы с такими коллекциями.
val emails: List<String?> = listOf("alice@mail.com", null, "bob@mail.com", null) // Получить только не‑null значения val validEmails: List<String> = emails.filterNotNull() println(validEmails) // [alice@mail.com, bob@mail.com]
filterNotNull() возвращает список с удалёнными null и меняет тип результата с List<String?> на List<String>.
Если нужно одновременно преобразовать элементы и отбросить null, используйте mapNotNull:
data class User(val name: String, val email: String?) val users = listOf(User("Alice", "alice@mail.com"), User("Bob", null)) // mapNotNull принимает (T) -> R? и возвращает List (без null) val emails = users.mapNotNull { it.email } // только не‑null email'ы (List<String>) // для сравнения: map { it.email } вернёт List<String?> val nullableEmails: List<String?> = users.map { it.email }
В Android такой подход часто применяется при работе с базой данных или сетью:
val result = repository.getUsers() // List<User?> val names = result.mapNotNull { it?.name }
Ошибка: забыть отфильтровать null и потом получить NPE при обращении к элементу списка.
Функция let в сочетании с безопасным вызовом – мощный инструмент для выполнения блока кода только с non‑null значением.
val user: User? = getUser() user?.let { u -> // здесь u имеет тип User (non‑null) println(u.name) updateUI(u) }
Блок let выполняется, только если значение не null. Внутри блока мы работаем с безопасным объектом.
val result = user?.let { fetchData(it) }?.let { process(it) }
Если fetchData(it) вернёт null, второй let не выполнится, и result будет null.
?.run – аналогичен let, но внутри доступен this (объект контекста).?.apply – часто используется для конфигурации объекта, возвращает сам объект.?.also – для побочных действий, возвращает объект. Пример:user?.also { log("Processing user: ${it.name}") }?.save()
Пример из Android (обновление UI только при непустых данных):
viewModel.userData.observe(this) { data -> data?.let { nameTextView.text = it.name emailTextView.text = it.email } }
Ошибка: путать let с run или использовать let без безопасного вызова (?.let) – тогда блок выполнится даже при null.
Приведение типов с помощью as может выбросить ClassCastException, если тип не совпадает. Kotlin предлагает безопасный оператор as?, который возвращает null при неудаче.
val obj: Any = "Hello" val str: String? = obj as? String // OK, str = "Hello" val number: Int? = obj as? Int // number = null
Часто используется с ?: для задания значения по умолчанию:
val serializable = intent.getSerializableExtra("key") val myData = serializable as? MyData ?: return // если не MyData, выходим
Примечание: метод getSerializableExtra устарел. В новых версиях Android используйте intent.getSerializableExtra("key", MyData::class.java) или Parcelable.
Внимание: as? не спасает от других исключений, например IndexOutOfBoundsException при обращении к списку. Поэтому всегда проверяйте границы. Kotlin предоставляет extension-функцию getOrNull:
val list: List<Int>? = getList() val element = list?.getOrNull(5) ?: 0 // безопасный доступ с запасным значением // list?.getOrNull(5) возвращает Int?, затем ?: преобразует в Int // list[5] выбросил бы IndexOutOfBoundsException при неверном индексеre>
Ошибка: использовать обычный as без проверки типа, особенно при работе с Intent extras или парсингом JSON.
@Nullable / @NonNull. Это поможет конвертеру создать корректные типы.Пример Java-класса с аннотациями:
public class User { private @Nullable String name; private @NonNull String email; public @Nullable String getName() { return name; } public @NonNull String getEmail() { return email; } }
После конвертации в Kotlin получим:
class User { val name: String? = null // @Nullable → String? val email: String = "" // @NonNull → String (non‑null) }
В Android Studio включите инспекции для поиска потенциальных NPE: File → Settings → Editor → Inspections → Kotlin → Nullability problems. Рекомендуется отметить все пункты.
Используйте ktlint и detekt с правилами для null safety. Они помогут автоматически находить опасные места, например использование !!.
В юнит‑тестах обязательно проверяйте функции с null в качестве входных данных. JUnit 5 поддерживает параметризованные тесты:
import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.NullSource import org.junit.jupiter.params.provider.ValueSource class NullSafetyTest { @ParameterizedTest @NullSource @ValueSource(strings = ["text", ""]) fun testLength(input: String?) { val result = getLengthOrDefault(input) assertNotNull(result) } }
Для этого требуется зависимость junit-jupiter-params.
Предупреждение: даже в тестах старайтесь избегать !!. Вместо этого используйте явные проверки, чтобы тесты явно показывали ожидаемое поведение.
Ошибка: игнорировать настройки инспекций и пропускать предупреждения о платформенных типах – они часто указывают на потенциальные проблемы.
Мы рассмотрели все основные инструменты Kotlin для борьбы с null. Сведём их в таблицу выбора подхода:
| Задача | Рекомендуемый приём | Когда НЕ использовать |
|---|---|---|
| Простой доступ к свойству/методу | ?. | Не подходит, если нужна сложная логика после проверки |
| Значение по умолчанию вместо null | ?: | Не подходит, если нужно выполнить блок кода при null (используйте ?: run) |
| Блок кода только при non‑null | ?.let { ... } | Для простых вызовов без блока лучше ?. |
| Несколько действий с объектом | if (obj != null) { ... } или ?.run | run не подходит, если нужен доступ к внешним переменным (используйте let) |
| Безопасное приведение типа | as? | Не заменяет проверку границ списка |
| Фильтрация null из коллекции | filterNotNull() / mapNotNull() | Если нужно сохранить null с преобразованием – используйте map |
| Ранний возврат при null | ?: return ... | Нельзя использовать в функциях, возвращающих значение, без явного возврата |
| Проверка var‑поля | локальная копия + if / ?. | Прямой if (field != null) может не работать из-за изменения поля |
| Проверка состояния (требование не‑null) | requireNotNull(), checkNotNull() | Используйте только для валидации аргументов/состояния, не для бизнес-логики |
| Отложенная инициализация | lateinit + ::field.isInitialized | Только для var, не для примитивов |
!!!! (non‑null asserted call) говорит компилятору: «я уверен, что здесь не null». Если вы ошибаетесь, приложение упадёт с NPE. Это почти всегда плохая практика. Избегайте его, за исключением очень узких ситуаций (например, в тестах или когда вы точно знаете, что null невозможен из‑за логики).
// Плохо: если name вдруг null, приложение упадёт val x: String? = null x!!.length // → NullPointerException // Пример из реальной практики: парсинг JSON без проверки val userId = json["id"]!!.toString() // если "id" отсутствует, упадёт // Лучше: val userId = json["id"]?.toString() ?: throw IllegalArgumentException("id required")
Retrofit: методы могут возвращать Response<User?>. Обрабатывайте null через ?: или ?.let.
val user = response.body()?.user ?: return
Room: сущности могут содержать nullable поля. В DAO методы могут возвращать LiveData<User?>.
val user = userDao.getUser(id) ?: createDefaultUser()
Вместо использования null для представления отсутствия значения часто лучше применить sealed class (переименуем в LoadState, чтобы избежать конфликта со встроенным Result):
sealed class LoadState { object Loading : LoadState() data class Success(val data: String) : LoadState() object Error : LoadState() } fun getStateText(state: LoadState): String = when (state) { LoadState.Loading -> "Загрузка..." is LoadState.Success -> "Данные: ${state.data}" LoadState.Error -> "Ошибка" }
Цепочки вызовов ?. могут создавать временные объекты (например, при упаковке результата в nullable). В критическом коде (например, в циклах) лучше использовать проверку if с локальной переменной.
String? и возвращает её длину или 0, если строка null. (Решение: fun safeLength(s: String?) = s?.length ?: 0)User(val name: String, val email: String?). Напишите функцию, которая возвращает email пользователя или "нет email". (Решение: fun getUserEmail(user: User) = user.email ?: "нет email")List<String?>. Получите список только не‑null значений, преобразовав их в верхний регистр. (Решение: val upperEmails = emails.filterNotNull().map { it.uppercase() })let выведите в лог имя пользователя только если оно не null. (Решение: user?.let { log("User: ${it.name}") })val body = response.body() ?: return handleError())Ответы на упражнения можно найти в комментариях или обсудить с коллегами.
Вопрос к читателям: какой случай работы с null вызвал у вас наибольшие трудности? Поделитесь опытом в комментариях, чтобы помочь другим разработчикам.
Надеемся, это руководство поможет вам писать чистый и надёжный Kotlin‑код без страха перед NullPointerException. Практикуйтесь, применяйте изученные приёмы в реальных проектах – и ваши программы станут значительно устойчивее.