Kotlin против Java: что лучше, когда дело доходит до нулевой безопасности?
17 февраля 2023 г.На прошлой неделе я был на конференции FOSDEM. FOSDEM отличается тем, что в нем есть несколько комнат, каждая из которых посвящена отдельной теме и организована командой. У меня было два разговора:
* Практическое введение в OpenTelemetry Tracing, в комнате разработки Monitoring and Observability
* Чего мне не хватает в Java, взгляд разработчика на Kotlin, в разделе Друзья OpenJDK< /em> комната для разработчиков
Второй доклад взят из предыдущего поста. Мартин Боннин сделал твит из одного слайда, и это вызвало настоящий ажиотаж, привлекший внимание даже Брайана Гетца.
В этом посте я хотел бы подробно рассказать о проблеме обнуления и о том, как она решается в Kotlin и Java, и добавить свои комментарии в ветку Twitter.
Обнуление
Думаю, все, кто занимается разработкой программного обеспечения с опытом работы более двух лет, слышали следующую цитату:
<цитата>Я называю это своей ошибкой на миллиард долларов. Это было изобретение нулевой ссылки в 1965 году. В то время я разрабатывал первую всеобъемлющую систему типов для ссылок в объектно-ориентированном языке (ALGOL W). Моя цель состояла в том, чтобы гарантировать, что любое использование ссылок должно быть абсолютно безопасным, с автоматической проверкой компилятором. Но я не мог устоять перед искушением указать нулевую ссылку просто потому, что это было так легко реализовать. Это привело к бесчисленным ошибкам, уязвимостям и системным сбоям, которые, вероятно, причинили миллиарды долларов боли и ущерба за последние сорок лет.
-- Тони Хоар
Основная идея null
заключается в том, что можно определить неинициализированную переменную. Если кто-то вызывает член такой переменной, среда выполнения находит адрес переменной в памяти... и не может ее разыменовать, потому что за ней ничего нет.
Нулевые значения встречаются во многих языках программирования под разными именами:
* Python имеет Нет
* JavaScript имеет null
* Как и Java, Scala и Kotlin.
* Руби имеет ноль
* и т.д.
Некоторые языки не допускают неинициализированные значения, например Rust.
Нулевая безопасность в Котлине
Как я уже упоминал, Kotlin допускает значения null
. Однако они встроены в систему типов. В Kotlin у каждого типа X
действительно есть два типа:
* X
, который не может быть нулевым. Никакая переменная типа X
не может быть null
. Компилятор гарантирует это.
котлин
val ул: строка = ноль
* Приведенный выше код не будет компилироваться.
* X?
, который может принимать значение NULL.
котлин
val ул: Строка? = ноль
Приведенный выше код компилируется.
Если Kotlin допускает значения null
, почему его сторонники рекламируют его нулевую безопасность? Компилятор отказывается вызывать члены для возможных нулевых значений, т. е. типов, допускающих значение null.
val str: String? = getNullableString()
val int: Int? = str.toIntOrNull() //1
- Не компилируется
Чтобы исправить приведенный выше код, нужно проверить, является ли переменная null
перед вызовом ее членов:
val str: String? = getNullableString()
val int: Int? = if (str == null) null
else str.toIntOrNull()
Приведенный выше подход довольно шаблонный, поэтому Kotlin предлагает оператор, безопасный для нуля, для достижения того же:
val str: String? = getNullableString()
val int: Int? = str?.toIntOrNull()
Нулевая безопасность в Java
Теперь, когда мы описали, как Kotlin управляет значениями null
, пришло время проверить, как это делает Java. Во-первых, в Java нет ни типов, не допускающих null, ни операторов, безопасных для null. Таким образом, каждая переменная потенциально может быть null
и должна считаться таковой.
var MyString str = getMyString(); //1
var Integer anInt = null; //2
if (str != null) {
anInt = str.toIntOrNull();
}
String
не имеет методаtoIntOrNull()
, поэтому давайте представим, чтоMyString
является типом-оболочкой и делегируетString
2. Необходима изменяемая ссылка.
Если вы объединяете несколько вызовов в цепочку, это еще хуже, так как каждое возвращаемое значение потенциально может быть null
. Чтобы быть в безопасности, нам нужно проверить, является ли результат вызова каждого метода null
. Следующий фрагмент кода может вызвать исключение NullPointerException
:
var baz = getFoo().getBar().getBaz();
Вот исправленная, но гораздо более подробная версия:
var foo = getFoo();
var bar = null;
var baz = null;
if (foo != null) {
bar = foo.getBar();
if (bar != null) {
baz = bar.getBaz();
}
}
По этой причине в Java 8 появился тип Optional. Необязательный
— это оболочка вокруг возможного нулевого значения. В других языках это называется Может быть
, Option
и т. д.
Разработчики языка Java советуют, чтобы метод возвращал:
* Введите X
, если X
не может быть null
* Введите Optional<X>
, если X
может быть null
Если мы изменим возвращаемый тип всех вышеперечисленных методов на Optional
, мы сможем переписать код нулевым безопасным способом — и получить неизменяемость сверху:
final var baz = getFoo().flatMap(Foo::getBar)
.flatMap(Bar::getBaz)
.orElse(null);
Мой главный аргумент в отношении этого подхода заключается в том, что сам Optional
может быть null
. Язык не гарантирует, что это не так. Кроме того, не рекомендуется использовать Optional
для входных параметров метода.
Чтобы справиться с этим, появились библиотеки на основе аннотаций:
| Проект | Пакет | Ненулевая аннотация | Обнуляемая аннотация |
|----|----|----|----|
| JSR 305 | javax.annotation
| @Nonnull
| @Nullable
|
| Spring | org.springframework.lang
| @NonNull
| @Nullable
|
| JetBrains | org.jetbrains.annotations
| @NotNull
| @Nullable
|
| Findbugs | edu.umd.cs.findbugs.annotations
| @NonNull
| @Nullable
|
| Eclipse | org.eclipse.jdt.annotation
| @NonNull
| @Nullable
|
| Среда проверки | org.checkerframework.checker.nullness.qual
| @NonNull
| @Nullable
|
| JSpecify | org.jspecify
| @NonNull
| @Nullable
|
| Ломбок | org.checkerframework.checker.nullness.qual
| @NonNull
| - |
Однако разные библиотеки работают по-разному:
* Spring выдает ПРЕДУПРЕЖДАЮЩИЕ сообщения во время компиляции.
* FindBugs требует специального исполнения.
* Lombok генерирует код, который добавляет проверку на null, но выдает NullPointerException
, если он все равно null
.
* и т.д.
Спасибо Себастьяну Делёзу за упоминание JSpecify, которое я раньше не знал. Это всеотраслевые усилия по устранению текущего беспорядка. Конечно, сразу приходит на ум знаменитый комикс XKCD:
Я все еще надеюсь, что все получится!
Заключение
Java появилась, когда null
-безопасность не была большой проблемой. Следовательно, случаи NullPointerException
являются обычным явлением. Единственное безопасное решение — обернуть каждый вызов метода проверкой null
. Это работает, но шаблонно и код становится труднее читать.
Доступны несколько альтернатив, но у них есть проблемы: они не являются пуленепробиваемыми, конкурируют друг с другом и работают совершенно по-разному.
Разработчики хвалят Kotlin за его null
-безопасность: это результат его механизма обработки null
, встроенного в структуру языка. Java никогда не сможет конкурировать с Kotlin в этом отношении, поскольку архитекторы языка Java ценят обратную совместимость выше безопасности кода.
Это их решение, и оно, вероятно, правильное, если вспомнить болезненность перехода с Python 2 на Python 3. Однако, как для разработчика, Kotlin кажется мне гораздо более привлекательным вариантом, чем Java.
Дальше:
Первоначально опубликовано на сайте A Java Geek 12 февраля 2023 г. em>
Оригинал