Главная фишка 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-свойства?