Безопасность при работе с null
Безопасность при работе с null — это функция Kotlin, предназначенная для значительного снижения риска появления null-ссылок, также известного как ошибка на миллиард долларов.
Одна из самых распространённых проблем во многих языках программирования, включая Java, заключается в том, что обращение к члену null-ссылки приводит к исключению null-ссылки. В Java это было бы эквивалентом NullPointerException, или, сокращённо, NPE.
Kotlin явно поддерживает возможность null в своей системе типов, то есть вы можете явно объявить, каким переменным или свойствам разрешено быть null. Кроме того, если вы объявляете переменные, не допускающие null, компилятор гарантирует, что эти переменные не смогут содержать значение null, предотвращая NPE.
Безопасность при работе с null в Kotlin позволяет создавать более безопасный код, обнаруживая потенциальные проблемы, связанные с null, во время компиляции, а не во время выполнения. Эта функция повышает надёжность, читаемость и удобство сопровождения кода, явно обозначая значения null, благодаря чему код легче понимать и поддерживать.
В Kotlin NPE может возникнуть только по следующим причинам:
Явный вызов
throw NullPointerException().Использование оператора утверждения ненулевого значения
!!.-
Несогласованность данных во время инициализации, например, когда:
Неинициализированный
this, доступный в конструкторе, используется где-либо ещё («утечкаthis»).Вызов конструктором суперкласса открытого члена, реализация которого в производном классе использует неинициализированное состояние.
-
Взаимодействие с Java:
Попытка обратиться к члену
null-ссылки платформенного типа.Проблемы с возможностью null для обобщённых типов. Например, фрагмент кода на Java добавляет
nullв KotlinMutableList<String>, для корректной обработки чего потребуетсяMutableList<String?>.Другие проблемы, вызванные внешним кодом на Java.
Типы, допускающие null, и типы, не допускающие null
В Kotlin система типов различает типы, которые могут содержать null (типы, допускающие null), и типы, которые не могут этого делать (типы, не допускающие null). Например, обычная переменная типа String не может содержать null:
fun main() {
//sampleStart
// Assigns a non-null string to a variable
var a: String = "abc"
// Attempts to re-assign null to the non-nullable variable
a = null
print(a)
// Null can not be a value of a non-null type String
//sampleEnd
}
Вы можете безопасно вызвать метод или обратиться к свойству a. Это гарантированно не приведёт к NPE, поскольку a — переменная, не допускающая null. Компилятор гарантирует, что a всегда содержит допустимое значение String, поэтому нет риска обратиться к его свойствам или методам, когда оно null:
fun main() {
//sampleStart
// Assigns a non-null string to a variable
val a: String = "abc"
// Returns the length of a non-nullable variable
val l = a.length
print(l)
// 3
//sampleEnd
}
Чтобы разрешить значения null, объявите переменную, добавив знак ? сразу после типа переменной. Например, можно объявить строку, допускающую null, написав String?. Это выражение задаёт для String тип, который может принимать null:
fun main() {
//sampleStart
// Assigns a nullable string to a variable
var b: String? = "abc"
// Successfully re-assigns null to the nullable variable
b = null
print(b)
// null
//sampleEnd
}
Если вы попытаетесь напрямую обратиться к length у b, компилятор сообщит об ошибке. Это происходит потому, что b объявлена как переменная, допускающая null, и может содержать значения null. Прямое обращение к свойствам значений, допускающих null, приводит к NPE:
fun main() {
//sampleStart
// Assigns a nullable string to a variable
var b: String? = "abc"
// Re-assigns null to the nullable variable
b = null
// Tries to directly return the length of a nullable variable
val l = b.length
print(l)
// Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String?
//sampleEnd
}
В приведённом выше примере компилятор требует использовать безопасные вызовы, чтобы проверить возможность null перед обращением к свойствам или выполнением операций. Существует несколько способов работы со значениями, допускающими null:
В следующих разделах приведены подробности и примеры инструментов и приёмов для обработки null.
Проверка на null с помощью условия if
При работе с типами, допускающими null, необходимо безопасно обрабатывать возможность null, чтобы избежать NPE. Один из способов — явно проверить возможность null с помощью условного выражения if.
Например, проверьте, является ли b значением null, а затем обратитесь к b.length:
fun main() {
//sampleStart
// Assigns null to a nullable variable
val b: String? = null
// Checks for nullability first and then accesses length
val l = if (b != null) b.length else -1
print(l)
// -1
//sampleEnd
}
В приведённом выше примере компилятор выполняет умное приведение типа, преобразуя тип из nullable String? в non-nullable String. Он также отслеживает информацию о выполненной проверке и разрешает вызов length внутри условного выражения if.
Поддерживаются и более сложные условия:
fun main() {
//sampleStart
// Assigns a nullable string to a variable
val b: String? = "Kotlin"
// Checks for nullability first and then accesses length
if (b != null && b.length > 0) {
print("String of length ${b.length}")
// String of length 6
} else {
// Provides alternative if the condition is not met
print("Empty string")
}
//sampleEnd
}
Обратите внимание, что приведённый выше пример работает только тогда, когда компилятор может гарантировать, что b не изменится между проверкой и использованием, как и в случае с предварительными условиями для умного приведения типа.
Оператор безопасного вызова
Оператор безопасного вызова ?. позволяет короче и безопаснее обрабатывать возможность null. Вместо выбрасывания NPE, если объект имеет значение null, оператор ?. просто возвращает null:
fun main() {
//sampleStart
// Assigns a nullable string to a variable
val a: String? = "Kotlin"
// Assigns null to a nullable variable
val b: String? = null
// Checks for nullability and returns length or null
println(a?.length)
// 6
println(b?.length)
// null
//sampleEnd
}
Выражение b?.length проверяет возможность null и возвращает b.length, если b не равно null, или null в противном случае. Тип этого выражения — Int?.
В Kotlin оператор ?. можно использовать как с переменными var, так и с val:
Переменная
var, допускающая null, может содержать значениеnull(например,var nullableValue: String? = null) или ненулевое значение (например,var nullableValue: String? = "Kotlin"). Если она содержит ненулевое значение, в любой момент его можно изменить наnull.Переменная
val, допускающая null, может содержать значениеnull(например,val nullableValue: String? = null) или ненулевое значение (например,val nullableValue: String? = "Kotlin"). Если она содержит ненулевое значение, впоследствии его нельзя изменить наnull.
Безопасные вызовы полезны в цепочках. Например, Боб — сотрудник, которому может быть назначен отдел. У этого отдела, в свою очередь, может быть другой сотрудник в качестве руководителя. Чтобы получить имя руководителя отдела Боба (если он есть), напишите следующее:
bob?.department?.head?.name
Эта цепочка возвращает null, если любое из её свойств имеет значение null.
Безопасный вызов также можно поместить в левую часть присваивания:
person?.department?.head = managersPool.getManager()
В приведённом выше примере, если один из получателей в цепочке безопасных вызовов имеет значение null, присваивание пропускается, а выражение справа вообще не вычисляется. Например, если person или person.department имеет значение null, функция не вызывается. Эквивалентный вариант того же безопасного вызова с условием if выглядит так:
if (person != null && person.department != null) {
person.department.head = managersPool.getManager()
}
Оператор Элвиса
При работе с типами, допускающими null, можно проверить наличие null и предоставить альтернативное значение. Например, если b не равно null, обратитесь к b.length. В противном случае верните альтернативное значение:
fun main() {
//sampleStart
// Assigns null to a nullable variable
val b: String? = null
// Checks for nullability. If not null, returns length. If null, returns 0
val l: Int = if (b != null) b.length else 0
println(l)
// 0
//sampleEnd
}
Вместо полной записи выражения if можно сделать это короче с помощью оператора Элвиса ?::
fun main() {
//sampleStart
// Assigns null to a nullable variable
val b: String? = null
// Checks for nullability. If not null, returns length. If null, returns a non-null value
val l = b?.length ?: 0
println(l)
// 0
//sampleEnd
}
Если выражение слева от ?: не равно null, оператор Элвиса возвращает его. В противном случае оператор Элвиса возвращает выражение справа. Выражение в правой части вычисляется только в том случае, если левая часть имеет значение null.
Поскольку throw и return являются выражениями в Kotlin, их также можно использовать в правой части оператора Элвиса. Это может быть удобно, например, при проверке аргументов функции:
fun foo(node: Node): String? {
// Checks for getParent(). If not null, it's assigned to parent. If null, returns null
val parent = node.getParent() ?: return null
// Checks for getName(). If not null, it's assigned to name. If null, throws exception
val name = node.getName() ?: throw IllegalArgumentException("name expected")
// ...
}
Оператор утверждения ненулевого значения
Оператор утверждения ненулевого значения !! преобразует любое значение в тип, не допускающий null.
Если применить оператор !! к переменной, значение которой не равно null, оно безопасно обрабатывается как значение типа, не допускающего null, и код выполняется обычным образом. Однако если значение равно null, оператор !! принудительно обрабатывает его как значение, не допускающее null, что приводит к NPE.
Если b не равно null и оператор !! возвращает его ненулевое значение (которое в этом примере является String), обращение к length выполняется корректно:
fun main() {
//sampleStart
// Assigns a nullable string to a variable
val b: String? = "Kotlin"
// Treats b as non-null and accesses its length
val l = b!!.length
println(l)
// 6
//sampleEnd
}
Если b равно null и оператор !! возвращает его ненулевое значение, возникает NPE:
fun main() {
//sampleStart
// Assigns null to a nullable variable
val b: String? = null
// Treats b as non-null and tries to access its length
val l = b!!.length
println(l)
// Exception in thread "main" java.lang.NullPointerException
//sampleEnd
}
Оператор !! особенно полезен, когда вы уверены, что значение не равно null и NPE возникнуть не может, но компилятор не может это гарантировать из-за определённых правил. В таких случаях можно использовать оператор !!, чтобы явно сообщить компилятору, что значение не равно null.
Получатель, допускающий null
Можно использовать функции-расширения с типом получателя, допускающим null, чтобы вызывать эти функции для переменных, которые могут иметь значение null.
Определив функцию-расширение для типа получателя, допускающего null, можно обрабатывать значения null непосредственно внутри функции, вместо того чтобы проверять наличие null в каждом месте её вызова.
Например, функцию-расширение .toString() можно вызывать для получателя, допускающего null. При вызове для значения null она безопасно возвращает строку "null", не выбрасывая исключение:
//sampleStart
fun main() {
// Assigns null to a nullable Person object stored in the person variable
val person: Person? = null
// Applies .toString to the nullable person variable and prints a string
println(person.toString())
// null
}
// Defines a simple Person class
data class Person(val name: String)
//sampleEnd
В приведённом выше примере, несмотря на то что person равно null, функция .toString() безопасно возвращает строку "null". Это может быть полезно при отладке и ведении журналов.
Если вы ожидаете, что функция .toString() вернёт строку, допускающую null (строковое представление или null), используйте оператор безопасного вызова ?.. Оператор ?. вызывает .toString(), только если объект не имеет значения null, в противном случае он возвращает null:
//sampleStart
fun main() {
// Assigns a nullable Person object to a variable
val person1: Person? = null
val person2: Person? = Person("Alice")
// Prints "null" if person is null; otherwise prints the result of person.toString()
println(person1?.toString())
// null
println(person2?.toString())
// Person(name=Alice)
}
// Defines a Person class
data class Person(val name: String)
//sampleEnd
Оператор ?. позволяет безопасно обрабатывать потенциальные значения null и при этом обращаться к свойствам или функциям объектов, которые могут иметь значение null.
Функция let
Чтобы обрабатывать значения null и выполнять операции только со значениями, не допускающими null, можно использовать оператор безопасного вызова ?. вместе с функцией let.
Это сочетание полезно для вычисления выражения, проверки результата на null и выполнения кода, только если результат не равен null, без ручных проверок на null:
fun main() {
//sampleStart
// Declares a list of nullable strings
val listWithNulls: List<String?> = listOf("Kotlin", null)
// Iterates over each item in the list
for (item in listWithNulls) {
// Checks if the item is null and only prints non-null values
item?.let { println(it) }
//Kotlin
}
//sampleEnd
}
Безопасные приведения типов
Обычный оператор Kotlin для приведения типов — оператор as. Однако обычные приведения могут привести к исключению, если объект не относится к целевому типу.
Для безопасного приведения типов можно использовать оператор as?. Он пытается привести значение к указанному типу и возвращает null, если значение не относится к этому типу:
fun main() {
//sampleStart
// Declares a variable of type Any, which can hold any type of value
val a: Any = "Hello, Kotlin!"
// Safe casts to Int using the 'as?' operator
val aInt: Int? = a as? Int
// Safe casts to String using the 'as?' operator
val aString: String? = a as? String
println(aInt)
// null
println(aString)
// "Hello, Kotlin!"
//sampleEnd
}
Приведённый выше код выводит null, потому что a не является Int, поэтому приведение безопасно завершается неудачей. Также выводится "Hello, Kotlin!", поскольку значение соответствует типу String? и безопасное приведение выполняется успешно.
Коллекции с типом, допускающим null
Если у вас есть коллекция элементов, допускающих null, и вы хотите оставить только ненулевые элементы, используйте функцию filterNotNull():
fun main() {
//sampleStart
// Declares a list containing some null and non-null integer values
val nullableList: List<Int?> = listOf(1, 2, null, 4)
// Filters out null values, resulting in a list of non-null integers
val intList: List<Int> = nullableList.filterNotNull()
println(intList)
// [1, 2, 4]
//sampleEnd
}
Что дальше?
Узнайте, как обрабатывать возможность null в Java и Kotlin.
Узнайте об обобщённых типах, которые определённо не допускают null.
© 2010–2026 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/null-safety.html