Урок по треку Kotlin 6 мин

Null-safety в Kotlin за 6 минут

Nullable-типы, ?., ?:, !!, smart cast, платформенные типы и lateinit.

Главная фишка Kotlin, о которой спросят первой: NullPointerException ловится компилятором. Для этого null встроен прямо в систему типов.

Nullable и non-null типы

var name: String = "Аня"
name = null            // ошибка компиляции

var nick: String? = null  // String? = "строка или null"
nick.length            // ошибка: nick может быть null

String и String? это разные типы. String является подтипом String?, поэтому non-null значение можно присвоить nullable-переменной, а наоборот нельзя без проверки.

Операторы работы с null

val len: Int? = nick?.length          // safe call: null, если nick == null
val len2: Int = nick?.length ?: 0     // Elvis: значение по умолчанию
val len3: Int = nick!!.length         // «я уверен»: NPE, если null
nick?.let { println("Ник: $it") }    // блок только для не-null

Elvis удобен и для раннего выхода, потому что return и throw это выражения типа Nothing:

fun greet(user: User?) {
    val name = user?.name ?: return
    val email = user.email ?: throw IllegalStateException("нет email")
}

!! используют редко: каждый !! это место, где программа может упасть. На ревью его обычно просят заменить на ?:, requireNotNull или нормальную проверку.

Smart cast

После проверки компилятор сам сужает тип:

fun len(s: String?): Int {
    if (s == null) return 0
    return s.length   // здесь s уже String
}

Smart cast работает для локальных val, локальных var, которые не меняются между проверкой и использованием, и для val-свойств без кастомного геттера из того же модуля. Для var-свойства класса не работает: его могли изменить из другого потока между проверкой и использованием. Решение: скопировать в локальную val или использовать ?.let.

lateinit и lazy

class ProfileFragment : Fragment() {
    private lateinit var adapter: ProfileAdapter   // инициализируем позже
    private val formatter by lazy { DateTimeFormatter.ofPattern("dd.MM") }
}
  • lateinit var только для non-null типов, не для примитивов (Int, Boolean), только var. Обращение до инициализации бросает UninitializedPropertyAccessException. Проверка: ::adapter.isInitialized.
  • by lazy для val: значение вычисляется при первом обращении и запоминается.

Платформенные типы

Java не говорит, может ли метод вернуть null. Такие значения в Kotlin имеют платформенный тип String!: компилятор разрешает работать с ним и как с String, и как с String?, и проверку переносит на вас.

val s = System.getProperty("user.name")  // String!
val n: String = s   // если там null, упадёт здесь же с NPE

Если Java-код размечен аннотациями @Nullable и @NotNull (JetBrains, AndroidX, JSpecify), Kotlin видит нормальные типы.

Коллекции и null

List<String?> это список, в котором могут быть null-элементы. List<String>? это сам список, который может быть null. Полезные функции: filterNotNull(), mapNotNull { }, firstOrNull(), getOrNull(i), orEmpty().

Когда NPE всё равно возможен

  • явный !!;
  • платформенные типы из Java;
  • обращение к lateinit до инициализации (тут будет другое исключение);
  • утечка this из конструктора: открытый метод, вызванный в init базового класса, видит ещё не проинициализированные поля наследника.

Частые ошибки

  • Злоупотреблять !! вместо Elvis и ранних выходов.
  • Писать if (x != null) x.foo() для var-свойства и удивляться ошибке smart cast.
  • Делать всё nullable «на всякий случай» и потом обвешиваться ?. по всему коду.
  • Путать List<String?> и List<String>?.

Что спросят на собесе

  • Как Kotlin защищает от NPE и в каких случаях NPE всё же возможен?
  • Чем ?., ?: и !! отличаются друг от друга?
  • Что такое платформенный тип?
  • Чем lateinit отличается от lazy?
  • Почему smart cast не работает для var-свойства?

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.