Kotlin против Java: что лучше, когда дело доходит до нулевой безопасности?

Kotlin против Java: что лучше, когда дело доходит до нулевой безопасности?

17 февраля 2023 г.

На прошлой неделе я был на конференции FOSDEM. FOSDEM отличается тем, что в нем есть несколько комнат, каждая из которых посвящена отдельной теме и организована командой. У меня было два разговора:

* Практическое введение в OpenTelemetry Tracing, в комнате разработки Monitoring and Observability

* Чего мне не хватает в Java, взгляд разработчика на Kotlin, в разделе Друзья OpenJDK< /em> комната для разработчиков

Второй доклад взят из предыдущего поста. Мартин Боннин сделал твит из одного слайда, и это вызвало настоящий ажиотаж, привлекший внимание даже Брайана Гетца.

https://twitter.com/martinbonnin/status/1622197657534857220

В этом посте я хотел бы подробно рассказать о проблеме обнуления и о том, как она решается в 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
  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();
}
  1. 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:

How standards proliferate by XKCD

Я все еще надеюсь, что все получится!

Заключение

Java появилась, когда null-безопасность не была большой проблемой. Следовательно, случаи NullPointerException являются обычным явлением. Единственное безопасное решение — обернуть каждый вызов метода проверкой null. Это работает, но шаблонно и код становится труднее читать.

Доступны несколько альтернатив, но у них есть проблемы: они не являются пуленепробиваемыми, конкурируют друг с другом и работают совершенно по-разному.

Разработчики хвалят Kotlin за его null-безопасность: это результат его механизма обработки null, встроенного в структуру языка. Java никогда не сможет конкурировать с Kotlin в этом отношении, поскольку архитекторы языка Java ценят обратную совместимость выше безопасности кода.

Это их решение, и оно, вероятно, правильное, если вспомнить болезненность перехода с Python 2 на Python 3. Однако, как для разработчика, Kotlin кажется мне гораздо более привлекательным вариантом, чем Java.

Дальше:


Первоначально опубликовано на сайте A Java Geek 12 февраля 2023 г.


Оригинал