Пустотные значения в Java и Kotlin
Пустотность — это возможность переменной содержать значение null. Когда переменная содержит null, попытка обратиться к её содержимому приводит к NullPointerException. Существует множество способов написания кода, чтобы минимизировать вероятность получения исключений NullPointerException.
Это руководство о различиях в подходах Java и Kotlin к обработке потенциально пустых переменных. Оно поможет вам мигрировать с Java на Kotlin и писать код в истинно Kotlin стиле.
Первая часть этого руководства охватывает самое важное различие — поддержку пустых типов в Kotlin и то, как Kotlin обрабатывает типы из Java-кода. Вторая часть, начиная с проверки результата вызова функции, рассматривает несколько конкретных случаев для объяснения определённых различий.
Узнайте больше о безопасности пустых значений в Kotlin.
Поддержка пустых типов
Самое важное различие между системами типов Kotlin и Java — это явная поддержка Kotlin для пустых типов. Это способ указать, какие переменные могут содержать значение null. Если переменная может содержать null, обращение к её методам небезопасно, так как это может вызвать NullPointerException. Kotlin запрещает такие вызовы на этапе компиляции, тем самым предотвращая множество потенциальных исключений. В момент выполнения объекты пустых типов и объекты непустых типов обрабатываются одинаково: пустой тип не является обёрткой для непустого типа. Все проверки выполняются на этапе компиляции. Это означает, что почти нет накладных расходов при работе с пустыми типами в Kotlin.
В Java, если вы не пишете проверок на пустоту, методы могут выбросить 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 все обычные типы по умолчанию непустые, если вы явно не помечаете их как пустые. Если вы не ожидаете, что a будет null, объявите функцию stringLength() следующим образом:
// Kotlin fun stringLength(a: String) = a.length
Параметр a имеет тип String, который в Kotlin означает, что он всегда должен содержать экземпляр String и не может содержать null. Пустые типы в Kotlin отмечаются знаком вопроса ?, например, String?. Ситуация с NullPointerException во время выполнения невозможна, если a является String, потому что компилятор обеспечивает правило, что все аргументы stringLength() не должны быть null.
Попытка передать значение null функции stringLength(a: String) приведёт к ошибке компиляции: «Null не может быть значением непустого типа String»:
Если вы хотите использовать эту функцию с любыми аргументами, включая null, используйте знак вопроса после типа аргумента String? и проверьте внутри тела функции, чтобы убедиться, что значение аргумента не является null:
// Kotlin fun stringLength(a: String?): Int = if (a != null) a.length else 0
После успешной проверки компилятор рассматривает переменную как если бы она была непустого типа String в области, где компилятор выполняет проверку.
Если вы не выполните эту проверку, код не будет компилироваться с сообщением: «Разрешены только вызовы безопасного обращения (?.) или непустого утверждения (!!.) для пустого получателя типа String?».
Вы можете написать это короче — использовать оператор безопасного вызова ?. (Сокращенная запись If-not-null), который позволяет объединить проверку на пустоту и вызов метода в одну операцию:
// Kotlin fun stringLength(a: String?): Int = a?.length ?: 0
Типы платформы
В Java вы можете использовать аннотации, показывающие, может ли переменная быть или не быть null. Такие аннотации не являются частью стандартной библиотеки, но вы можете добавить их отдельно. Например, вы можете использовать аннотации JetBrains @Nullable и @NotNull (из пакета org.jetbrains.annotations) или аннотации из Eclipse (org.eclipse.jdt.annotation). Kotlin может распознать такие аннотации при вызове Java-кода из Kotlin-кода и обработает типы в соответствии с их аннотациями.
Если в вашем Java-коде нет этих аннотаций, то Kotlin будет обрабатывать Java-типы как платформенные типы. Но поскольку Kotlin не имеет информации о пустотности таких типов, его компилятор позволит все операции над ними. Вам нужно будет решить, выполнять ли проверки на пустоту, потому что:
Так же, как и в Java, вы получите
NullPointerExceptionпри попытке выполнения операции сnull.Компилятор не выделит излишние проверки на пустоту, что он обычно делает при выполнении безопасной операции с пустым значением над значением непустого типа.
Узнайте больше о вызове Java из Kotlin в отношении безопасности пустых значений и типов платформы.
Проверка результата вызова функции
Одна из самых распространённых ситуаций, где вам нужно проверить на null — это получение результата из вызова функции.
В следующем примере есть два класса, Order и Customer. Order содержит ссылку на экземпляр Customer. Функция findOrder() возвращает экземпляр класса Order или null если заказ не найден. Цель — обработать экземпляр клиента полученного заказа.
Вот классы на Java:
//Java
record Order (Customer customer) {}
record Customer (String name) {}
В Java вызовите функцию и выполните проверку на пустоту результата, чтобы продолжить обращение к необходимому свойству:
// 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)
}
Используйте оператор безопасного вызова ?. (Сокращённая запись If-not-null) в сочетании с любой из функций области видимости из стандартной библиотеки. Обычно используется функция let:
// Kotlin
val order = findOrder()
order?.let {
processCustomer(it.customer)
}
Вот более короткая версия того же:
// Kotlin findOrder()?.customer?.let(::processCustomer)
Значения по умолчанию вместо null
Проверка на null часто используется в сочетании с установкой значения по умолчанию в случае успешной проверки на пустоту.
Java-код с проверкой на пустоту:
// Java
Order order = findOrder();
if (order == null) {
order = new Order(new Customer("Antonio"))
}
Для выражения того же на Kotlin используйте оператор Эльвиса (сокращённая запись If-not-null-else):
// 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
}
Что дальше?
Просмотрите другие Kotlin-идиомы.
Узнайте, как преобразовать существующий код Java в Kotlin с помощью конвертера Java-Kotlin (J2K).
-
Ознакомьтесь с другими руководствами по миграции:
Если у вас есть любимая идиома, поделитесь ею с нами, отправив запрос на добавление!
© 2010–2023 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