Kotlin Null-Safety Против Classloader

Недавно у меня было интервью, и одним из вопросов был этот загадочный экземпляр: «В чем главное преимущество системы типов Kotlin перед JavaЭ» Честно говоря, определить, какое преимущество считать главным, оказалось для меня неразрешимой задачей: Ничего, отсутствие Wildcard и First-Class Functions вместо Java-костыля с Functional Interface (имеется в виду 8-я версия Java) не помогло.

не занять первое место в личном топе интервьюера, который мне предложили угадать.

Оказалось, что главное в Котлине — это возможность объявить Nullable Type и подход Null Safety (замечу, что по моему опыту собственные или библиотекиOptional или Maybe решают эту проблему и пишутся за 10 минут на Java 7. Там также являются аннотациями, допускающими значение Null, которые позволяют проверять поля во время компиляции.

Короче говоря, существует множество способов принудительно проверять значения Null в простой Java. Ну да ладно).

Но речь идет не о странных вопросах, связанных со вкусовыми предпочтениями интервьюеров относительно синтаксического сахара.

Дело в том, что Null Safety в Kotlin можно взломать, не выходя из его безопасного купола в суровый дикий мир Java и Null-References. Как? Короче говоря: ClassLoader ведет себя интересным образом при попытке загрузить статические поля классов, которые рекурсивно ссылаются на классы друг друга.

Ниже приведены примеры кода и подробное объяснение того, как он обманывает проверки Nullable. Искренне надеюсь, что для статьи не потребуются специфические знания Java/Kotlin — все детали я объясню по ходу дела и сократим рассмотрение до 10 минут. Давай начнем.




Я не буду тянуть кота за хвост. Вот код:
  
  
  
  
  
  
  
   

class ClassToLoad1() { val classToLoad2 = ClassToLoad1.classToLoad2 //Creating single instance of object companion object { val classToLoad2 = ClassToLoad2() } } class ClassToLoad2() { val classToLoad1 = ClassToLoad2.classToLoad1 companion object { val classToLoad1 = ClassToLoad1() } } fun main() { val check = ClassToLoad1() val classToLoadRecurciveRef = check.classToLoad2.classToLoad1.classToLoad2 println(classToLoadRecurciveRef) //null classToLoadRecurciveRef.classToLoad1 //Throws NPE }

Ни одной строчки Java-кода, ни одного предупреждения или NPE в результате выполнения.

Почему это? Роковое стечение нескольких обстоятельств: Рекурсивная ссылка, статическая инициализация и не самый явный контракт поведения ClassLoader в JVM в таких случаях.






//Рекурсивные ссылки

Давайте отрежем из нашего примера все, кроме рекурсивного обращения классов к самим себе.

Базовый пример — один класс, который хочет получить себя в качестве экземпляра.

Зачем это вообще делать? Самый распространенный случай, когда вы видели такую структуру, — это связанный список (который, кстати, тоже популярен на собеседованиях).

Любая другая ссылочная структура данных, например узел в дереве, также будет ссылаться на себя.

И если вам действительно нравится писать графики, вы можете напрямую столкнуться с таким кодом.



class SelfClassLoad(val s: SelfClassLoad) // warning: Constructor has non-null self reference parameter

Ой, у нас предупреждение! И это существует по очень понятной причине — вы не можете использовать этот класс:

class SelfClassLoad(val s: SelfClassLoad) // warning: Constructor has non-null self reference parameter fun main() { SelfClassLoad( SelfClassLoad( SelfClassLoad( SelfClassLoad( SelfClassLoad( //No value passed for parameter 's' ) ) ) ) ) }

Также было бы интересно немного изменить пример:

class SelfClassLoad(){ val s = SelfClassLoad() } fun main() { SelfClassLoad() //Stackoverflow error }

По понятным причинам мы просто получим StackOverflow — бесконечная инициализация себя ни к чему хорошему не приводит. Но получим ли мы предупреждение, если попытаемся создать рекурсивную комбинацию двух классов?

class SelfClassLoad1(val s2: SelfClassLoad2) class SelfClassLoad2(val s1: SelfClassLoad1) //No warnings

В этом случае больше не выдается предупреждение.

Вопрос на эту тему уже создавался: https://youtrack.jetbrains.com/issue/KTIJ-14263 И это первый шаг к нашему НП?.






//Статическая инициализация

Что происходит в Котлине, когда вы создаете объект-компаньон? Если вы пытались вызвать аналогичный код из Java, то знаете, что ваш класс будет содержать статическую ссылку на сопутствующий объект. Если вы попытаетесь перевести Kotlin на Java, вы получите что-то вроде этого:

public final class ClassToLoad1 { @NotNull private static final ClassToLoad2 classToLoad2; @NotNull public static final Companion Companion; //Here, it's "static" @NotNull public final ClassToLoad2 getClassToLoad2() { return this.classToLoad2$1; } static { Companion = new Companion(null); classToLoad2 = new ClassToLoad2(); } public static final class Companion { @NotNull public final ClassToLoad2 getClassToLoad2() { return classToLoad2; } private Companion() { } public Companion(DefaultConstructorMarker $constructor_marker) { this(); } } }

Опытные Java-разработчики уже поняли, к чему это идет (если, конечно, не поняли с самого начала), но для широкой аудитории оставлю пояснение: Ключевое слово статический , делает именно то, что говорит — объект становится статическим.

А в мире JVM это означает, что этот объект находится в особой области памяти — Permanent Generation Space. Это означает 2 вещи: 1. Статический объект будет жить до тех пор, пока жива его JVM. 2. Он будет инициализирован (помещен в PermGen) 1 раз.

(Да-да, бывают случаи, когда это не так, в настоящее время это выходит за рамки) Для тех, кто помнит про синглтоны: Возможно, вы сейчас думаете что-то вроде: «Зачем тогда нужен сложный синглтон с Syncronized, если статики достаточноЭ» Ответ: синхронизация и другие методы необходимы, когда вам нужно лениво инициализировать синглтон.



class Singleton1{ public final static Singleton1 singleton = new Singleton1() } //Warranty to be single time initialized, but will init on class init.



class Singleton2{ private static Singleton2 singleton; public static void getSingleton(){

Теги: #java #Ненормальное программирование #Kotlin #jvm #отладка #Swift #компиляторы #bug #compiler #recursive #classloader

Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.