Типы, допускающие значение null, в Java и Kotlin
Возможность принимать значение null — это способность переменной хранить значение null. Если переменная содержит null, попытка разыменовать её приводит к NullPointerException. Существует множество способов писать код, чтобы свести к минимуму вероятность возникновения исключений нулевого указателя.
В этом руководстве рассматриваются различия между подходами Java и Kotlin к обработке переменных, которые могут иметь значение null. Оно поможет вам перейти с Java на Kotlin и писать код в аутентичном стиле Kotlin.
В первой части руководства рассматривается самое важное различие — поддержка типов, допускающих значение null, в Kotlin и обработка Kotlin типов из кода Java. Во второй части, начиная с раздела Проверка результата вызова функции, рассматриваются несколько конкретных случаев, чтобы объяснить некоторые различия.
Подробнее о безопасности при работе с null в Kotlin.
Поддержка типов, допускающих значение null
Самое важное различие между системами типов Kotlin и Java — это явная поддержка в Kotlin типов, допускающих значение null. Это способ указать, какие переменные могут хранить значение null. Если переменная может быть null, вызывать её метод небезопасно, поскольку это может привести к NullPointerException. Kotlin запрещает такие вызовы во время компиляции и тем самым предотвращает множество возможных исключений. Во время выполнения объекты типов, допускающих значение null, и объекты типов, не допускающих его, обрабатываются одинаково: тип, допускающий значение null, не является обёрткой для типа, не допускающего значение null. Все проверки выполняются во время компиляции. Это означает, что работа с типами, допускающими значение null, в Kotlin почти не влечёт дополнительных затрат во время выполнения.
В Java, если не выполнять проверки на null, методы могут выбросить NullPointerException:
// Java
int stringLength(String a) {
return a.length();
}
void main() {
stringLength(null); // Throws a `NullPointerException`
}
Этот вызов выведет следующее:
java.lang.NullPointerException: Cannot invoke "String.length()" because "a" is null
at test.java.Nullability.stringLength(Nullability.java:8)
at test.java.Nullability.main(Nullability.java:12)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
В Kotlin все обычные типы по умолчанию не допускают значение null, если вы явно не укажете обратное. Если вы не ожидаете, что a будет null, объявите функцию stringLength() следующим образом:
// Kotlin fun stringLength(a: String) = a.length
Параметр a имеет тип String, что в Kotlin означает, что он всегда должен содержать экземпляр String и не может содержать null. Типы, допускающие значение null, в Kotlin обозначаются вопросительным знаком ?, например, String?. Ситуация, когда во время выполнения возникает NullPointerException, невозможна, если a является String, поскольку компилятор обеспечивает соблюдение правила, согласно которому все аргументы stringLength() не должны быть null.
Попытка передать значение null функции stringLength(a: String) приведёт к ошибке компиляции: «Null не может быть значением типа String, не допускающего null»:
Если вы хотите использовать эту функцию с любыми аргументами, в том числе null, поставьте вопросительный знак после типа аргумента String? и проверьте в теле функции, что значение аргумента не равно null:
// Kotlin fun stringLength(a: String?): Int = if (a != null) a.length else 0
После успешного прохождения проверки компилятор считает переменную типом, не допускающим значение null, String в области видимости, где выполняется проверка.
Если не выполнить эту проверку, код не скомпилируется и появится следующее сообщение: «Для безопасного вызова (?.) или вызова с утверждением, что значение не равно null (!!.) требуется получатель, допускающий значение null, типа String?».
Ту же операцию можно записать короче: используйте оператор безопасного вызова ?. (сокращённая запись «если не null»), который позволяет объединить проверку на null и вызов метода в одну операцию:
// Kotlin fun stringLength(a: String?): Int = a?.length ?: 0
Платформенные типы
В Java можно использовать аннотации, указывающие, может ли переменная быть null. Такие аннотации не входят в стандартную библиотеку, но их можно подключить отдельно. Например, можно использовать аннотации JetBrains @Nullable и @NotNull (из пакета org.jetbrains.annotations), аннотации из JSpecify (org.jspecify.annotations) или Eclipse (org.eclipse.jdt.annotation). Kotlin распознаёт такие аннотации при вызове кода Java из кода Kotlin и обрабатывает типы в соответствии с аннотациями.
Если в коде Java нет этих аннотаций, Kotlin будет рассматривать типы Java как платформенные типы. Но поскольку Kotlin не располагает информацией о возможности принимать значение null для таких типов, его компилятор разрешит выполнять с ними любые операции. Вам нужно будет самостоятельно решить, выполнять ли проверки на null, поскольку:
Как и в Java, вы получите
NullPointerException, если попытаетесь выполнить операцию надnull.Компилятор не будет отмечать избыточные проверки на null, хотя обычно он делает это, когда вы выполняете операцию с безопасной обработкой null для значения типа, не допускающего null.
Подробнее о вызове Java из Kotlin с точки зрения безопасности при работе с null и платформенных типов.
Поддержка типов, определённо не допускающих значение null
В Kotlin, если вы хотите переопределить метод Java, принимающий @NotNull в качестве аргумента, вам понадобятся типы Kotlin, определённо не допускающие значение null.
Например, рассмотрим следующий метод load() в Java:
import org.jetbrains.annotations.*;
public interface Game<T> {
public T save(T x) {}
@NotNull
public T load(@NotNull T x) {}
}
Чтобы успешно переопределить метод load() в Kotlin, необходимо объявить T1 как тип, определённо не допускающий значение null (T1 & Any):
interface ArcadeGame<T1> : Game<T1> {
override fun save(x: T1): T1
// T1 is definitely non-nullable
override fun load(x: T1 & Any): T1 & Any
}
Подробнее о типах с параметрами, определённо не допускающих значение null.
Проверка результата вызова функции
Одна из самых распространённых ситуаций, в которых требуется проверять на null, — получение результата вызова функции.
В следующем примере есть два класса: Order и Customer. Order содержит ссылку на экземпляр Customer. Функция findOrder() возвращает экземпляр класса Order или null, если не может найти заказ. Цель — обработать экземпляр клиента из найденного заказа.
Вот эти классы на Java:
//Java
record Order (Customer customer) {}
record Customer (String name) {}
В Java вызовите функцию и проверьте результат условием «если не null», прежде чем обращаться к нужному свойству:
// Java
Order order = findOrder();
if (order != null) {
processCustomer(order.getCustomer());
}
При непосредственном преобразовании приведённого выше кода Java в Kotlin получится следующее:
// Kotlin
data class Order(val customer: Customer)
data class Customer(val name: String)
val order = findOrder()
// Direct conversion
if (order != null){
processCustomer(order.customer)
}
Используйте оператор безопасного вызова ?. (сокращённую запись «если не null») вместе с любой из функций области видимости стандартной библиотеки. Обычно для этого используется функция let:
// Kotlin
val order = findOrder()
order?.let {
processCustomer(it.customer)
}
Ту же операцию можно записать короче:
// Kotlin findOrder()?.customer?.let(::processCustomer)
Значения по умолчанию вместо null
Проверка на null часто используется вместе с установкой значения по умолчанию на случай успешного прохождения проверки на null.
Код Java с проверкой на null:
// Java
Order order = findOrder();
if (order == null) {
order = new Order(new Customer("Antonio"))
}
Чтобы выразить то же самое на Kotlin, используйте оператор Элвиса (сокращённую запись «если не null, иначе»):
// Kotlin
val order = findOrder() ?: Order(Customer("Antonio"))
Функции, возвращающие значение или null
В Java при работе с элементами списка нужно быть осторожным. Перед тем как использовать элемент, всегда проверяйте, существует ли элемент с данным индексом:
// Java var numbers = new ArrayList<Integer>(); numbers.add(1); numbers.add(2); System.out.println(numbers.get(0)); //numbers.get(5) // Exception!
В стандартной библиотеке Kotlin часто встречаются функции, названия которых указывают, могут ли они возвращать значение null. Особенно часто такие функции встречаются в API коллекций:
fun main() {
//sampleStart
// Kotlin
// The same code as in Java:
val numbers = listOf(1, 2)
println(numbers[0]) // Can throw IndexOutOfBoundsException if the collection is empty
//numbers.get(5) // Exception!
// More abilities:
println(numbers.firstOrNull())
println(numbers.getOrNull(5)) // null
//sampleEnd
}
Агрегатные операции
Если нужно получить наибольший элемент или null, когда элементов нет, в Java для этого используется Stream API:
// Java
var numbers = new ArrayList<Integer>();
var max = numbers.stream().max(Comparator.naturalOrder()).orElse(null);
System.out.println("Max: " + max);
В Kotlin используйте агрегатные операции:
// Kotlin
val numbers = listOf<Int>()
println("Max: ${numbers.maxOrNull()}")
Подробнее о коллекциях в Java и Kotlin.
Безопасное приведение типов
Если нужно безопасно привести тип, в Java для этого используется оператор instanceof, после чего проверяется результат:
// Java
int getStringLength(Object y) {
return y instanceof String x ? x.length() : -1;
}
void main() {
System.out.println(getStringLength(1)); // Prints `-1`
}
Чтобы избежать исключений в Kotlin, используйте оператор безопасного приведения типа as?, который при неудаче возвращает null:
// Kotlin
fun main() {
println(getStringLength(1)) // Prints `-1`
}
fun getStringLength(y: Any): Int {
val x: String? = y as? String // null
return x?.length ?: -1 // Returns -1 because `x` is null
}
При переносе кода Java в Kotlin сначала можно использовать обычный оператор приведения типа as с типом, допускающим значение null, чтобы сохранить исходную семантику кода. Однако мы рекомендуем адаптировать код и использовать оператор безопасного приведения типа as? — это более безопасный и идиоматичный подход. Например, если у вас есть следующий код на Java:
public class UserProfile {
Object data;
public static String getUsername(UserProfile profile) {
if (profile == null) {
return null;
}
return (String) profile.data;
}
}
При непосредственном переносе с оператором as получится следующее:
class UserProfile(var data: Any? = null)
fun getUsername(profile: UserProfile?): String? {
if (profile == null) {
return null
}
return profile.data as String?
}
Здесь profile.data приводится к строке, допускающей значение null, с помощью as String?.
Рекомендуем пойти дальше и использовать as? String для безопасного приведения значения. При неудаче этот подход возвращает null вместо того, чтобы выбрасывать ClassCastException:
class UserProfile(var data: Any? = null) fun getUsername(profile: UserProfile?): String? = profile?.data as? String
В этой версии выражение if заменено на оператор безопасного вызова ?., который безопасно получает доступ к свойству data перед попыткой приведения типа.
Что дальше?
Ознакомьтесь с другими идиомами Kotlin.
Узнайте, как преобразовать существующий код Java в Kotlin с помощью конвертера Java в Kotlin (J2K).
-
Ознакомьтесь с другими руководствами по переносу кода:
Если у вас есть любимая идиома, поделитесь ею с нами, отправив pull request!
© 2010–2026 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/java-to-kotlin-nullability-guide.html