Kotlin под капотом — смотрим декомпилированный байткод

от автора

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

Я специально упущу довольно избитые и известные моменты так как, наверное, нет смысла в сотый раз писать о генерации геттеров/сеттеров для var и подобных вещах. Итак начнем.

Как посмотреть декомпилированный байткод в Intellij Idea?

Довольно просто — достаточно открыть нужный файл и выбрать в меню Tools -> Kotlin -> Show Kotlin Bytecode

image

Далее в появившемся окне просто нажимаем Decompile

Для просмотра будет использоваться версия Kotlin 1.3-RC.
Теперь, наконце-то, перейдем к основной части.

object

Kotlin

object Test

Decompiled Java

public final class Test {    public static final Test INSTANCE;     static {       Test var0 = new Test();       INSTANCE = var0;    } }

Я полагаю все, кто имеет дело с Kotlin знает, что object создает синглтон. Однако, далеко не всем очевидно какой именно синглтон создается и является ли он потокобезопасным.

По декомпилированному коду видно, что полученный синглтон похож на eager реализацию синглтона, он создается в тот момент, когда класслоудер загружает класс. Потокобезопасным он является лишь условно — с одной стороны static блок выполняется при загрузке класслоудером, что само по себе потокобезопасно. С другой стороны, если класслоудеров больше одного, то и одним экземпляром можно не отделаться.

extensions

Kotlin

fun String.getEmpty(): String {     return "" }

Decompiled Java

public final class TestKt {    @NotNull    public static final String getEmpty(@NotNull String $receiver) {       Intrinsics.checkParameterIsNotNull($receiver, "receiver$0");       return "";    } }

Тут в общем все понятно — экстеншны являются просто синтаксическим сахарком и компилируются в обычный статический метод.

Если кого-то смутила строчка с Intrinsics.checkParameterIsNotNull, то и там все прозрачно — во всех функциях с не nullable аргументами Kotlin добавляет проверку на null и кидает исключение если вы подсунули свинью null, хотя в аргументах обещали этого не делать. Выглядит это так:

public static void checkParameterIsNotNull(Object value, String paramName) {     if (value == null) {         throwParameterIsNullException(paramName);     } }

Что характерно, если написать не функцию, а extension property

val String.empty: String     get() {     return "" }

То в результате мы получим ровно то же самое, что получили для метода String.getEmpty()

inline

Kotlin

inline fun something() {     println("hello") }  class Test {     fun test() {         something()     } }

Decompiled Java

public final class Test {    public final void test() {       String var1 = "hello";       System.out.println(var1);    } }  public final class TestKt {    public static final void something() {       String var1 = "hello";       System.out.println(var1);    } } 

С инлайном все довольно просто — функция, помеченная как inline просто целиком и полностью вставляется в то место, откуда ее вызвали. Что интересно — она также сама по себе компилится в статику, вероятно, для возможности interoperability с Java.

Вся мощь инлайна раскрывается в тот момент, когда в аргументах значится лямбда:

Kotlin

inline fun something(action: () -> Unit) {     action()     println("world") }  class Test {     fun test() {         something {             println("hello")         }     } }

Decompiled Java

public final class Test {    public final void test() {       String var1 = "hello";       System.out.println(var1);       var1 = "world";       System.out.println(var1);    } }  public final class TestKt {    public static final void something(@NotNull Function0 action) {       Intrinsics.checkParameterIsNotNull(action, "action");       action.invoke();       String var2 = "world";       System.out.println(var2);    } }

В нижней части опять видна статика, а в верхней видно, что лямбда в аргументе функции также инлайнится, а не создает дополнительный анонимный класс, как это было бы в Java.

Примерно на этом познания inline в Kotlin у многих заканчиваются, но есть еще 2 интересных момента, а именно noinline и crossinline. Это ключевые слова, которые можно приставить к лямбде являющейся аргументом в инлайн функции.

Kotlin

inline fun something(noinline action: () -> Unit) {     action()     println("world") }  class Test {     fun test() {         something {             println("hello")         }     } }

Decompiled Java

public final class Test {    public final void test() {       Function0 action$iv = (Function0)null.INSTANCE;       action$iv.invoke();       String var2 = "world";       System.out.println(var2);    } }  public final class TestKt {    public static final void something(@NotNull Function0 action) {       Intrinsics.checkParameterIsNotNull(action, "action");       action.invoke();       String var2 = "world";       System.out.println(var2);    } }

При такой записи IDE начинает указывать, что такой инлайн бесполезен чуть менее чем полностью. А компилирует ровно в то же, что и Java — создает Function0. Почему декомпилировалось со странным (Function0)null.INSTANCE; — я без понятия, вероятнее всего это баг декомпилятора.

crossinline в свою очередь делает ровно то же, что и обычный inline (то есть если перед лямбдой в аргументе не писать вообще ничего), за небольшим исключением — в лямбде нельзя писать return, что необходимо для блокирования возможности внезапно завершить функцию, вызывающую inline. В смысле написать-то можно, но во-первых IDE будет ругаться, а во вторых при компиляции получим

‘return’ is not allowed here

Впрочем, байткод у crossinline не отличается от дефолтного инлайна — ключевое слово используется только компилятором.

infix

Kotlin

infix fun Int.plus(value: Int): Int {     return this+value }  class Test {     fun test() {         val result = 5 plus 3     } }

Decompiled Java

public final class Test {    public final void test() {       int result = TestKt.plus(5, 3);    } }  public final class TestKt {    public static final int plus(int $receiver, int value) {       return $receiver + value;    } }

Инфиксные функции компилируются как и экстеншны в обычную статику

tailrec

Kotlin

tailrec fun factorial(step:Int, value: Int = 1):Int {     val newValue = step*value     return if (step == 1) newValue else factorial(step - 1,newValue) }

Decompiled Java

public final class TestKt {    public static final int factorial(int step, int value) {       while(true) {          int newValue = step * value;          if (step == 1) {             return newValue;          }           int var10000 = step - 1;          value = newValue;          step = var10000;       }    }     // $FF: synthetic method    public static int factorial$default(int var0, int var1, int var2, Object var3) {       if ((var2 & 2) != 0) {          var1 = 1;       }        return factorial(var0, var1);    } }

tailrec является довольно занятной штукой. Как видно из кода рекурсия просто перегоняется в куда менее читаемый цикл, зато разработчик может спать спокойно, так как ничего не вылетит со Stackoverflow в самый неприятный момент. Другое дело в реальной жизни найти применение tailrec получится редко.

reified

Kotlin

inline fun <reified T>something(value: Class<T>) {     println(value.simpleName) } 

Decompiled Java

public final class TestKt {    private static final void something(Class value) {       String var2 = value.getSimpleName();       System.out.println(var2);    } }

Вообще про саму концепцию reified и для чего это надо можно написать целую статью. Если вкрадце, то доступ к самому типу в Java в compile time невозможен, т.к. до компиляции Java знать не знает что там будет вообще. Котлин — другое дело. Ключевое слово reified может быть использовано только в inline функциях, которые как уже отмечалось просто копируются и вставляются в нужные места, таким образом уже во время «вызова» функции компилятор уже в курсе что именно там за тип и может модифицировать байткод.

Следует обратить внимание на то, что в байткоде компилируется статичная функция с приватным уровнем доступа, а значит из Java такое дернуть не получится. К слову из-за reified в рекламе Kotlin «100% interoperable with Java and Android» получается как минимум неточность.

image

Может все-таки 99%?

init

Kotlin

class Test {     constructor()     constructor(value: String)          init {         println("hello")     } }

Decompiled Java

public final class Test {    public Test() {       String var1 = "hello";       System.out.println(var1);    }     public Test(@NotNull String value) {       Intrinsics.checkParameterIsNotNull(value, "value");       super();       String var2 = "hello";       System.out.println(var2);    } }

В целом с init все просто — это обычная inline функция, которая отрабатывает до вызова кода самого конструктора.

data class

Kotlin

data class Test(val argumentValue: String, val argumentValue2: String) {     var innerValue: Int = 0 }

Decompiled Java

public final class Test {    private int innerValue;    @NotNull    private final String argumentValue;    @NotNull    private final String argumentValue2;     public final int getInnerValue() {       return this.innerValue;    }     public final void setInnerValue(int var1) {       this.innerValue = var1;    }     @NotNull    public final String getArgumentValue() {       return this.argumentValue;    }     @NotNull    public final String getArgumentValue2() {       return this.argumentValue2;    }     public Test(@NotNull String argumentValue, @NotNull String argumentValue2) {       Intrinsics.checkParameterIsNotNull(argumentValue, "argumentValue");       Intrinsics.checkParameterIsNotNull(argumentValue2, "argumentValue2");       super();       this.argumentValue = argumentValue;       this.argumentValue2 = argumentValue2;    }     @NotNull    public final String component1() {       return this.argumentValue;    }     @NotNull    public final String component2() {       return this.argumentValue2;    }     @NotNull    public final Test copy(@NotNull String argumentValue, @NotNull String argumentValue2) {       Intrinsics.checkParameterIsNotNull(argumentValue, "argumentValue");       Intrinsics.checkParameterIsNotNull(argumentValue2, "argumentValue2");       return new Test(argumentValue, argumentValue2);    }     // $FF: synthetic method    @NotNull    public static Test copy$default(Test var0, String var1, String var2, int var3, Object var4) {       if ((var3 & 1) != 0) {          var1 = var0.argumentValue;       }        if ((var3 & 2) != 0) {          var2 = var0.argumentValue2;       }        return var0.copy(var1, var2);    }     @NotNull    public String toString() {       return "Test(argumentValue=" + this.argumentValue + ", argumentValue2=" + this.argumentValue2 + ")";    }     public int hashCode() {       return (this.argumentValue != null ? this.argumentValue.hashCode() : 0) * 31 + (this.argumentValue2 != null ? this.argumentValue2.hashCode() : 0);    }     public boolean equals(@Nullable Object var1) {       if (this != var1) {          if (var1 instanceof Test) {             Test var2 = (Test)var1;             if (Intrinsics.areEqual(this.argumentValue, var2.argumentValue) && Intrinsics.areEqual(this.argumentValue2, var2.argumentValue2)) {                return true;             }          }           return false;       } else {          return true;       }    } }

Честно говоря вообще не хотелось упоминать дата классы, о которых уже столько сказано, но тем не менее есть пара моментов заслуживающих внимания. Во-первых стоит заметить, что в equals/hashCode/copy/toString попадают только те переменные, которые были переданы в конструктор. На вопрос почему так — Андрей Бреслав ответил, что брать еще и поля не переданные в конструкторе сложно и запарно. К слову от дата класса нельзя наследоваться, правда только потому, что при наследовании нагенеренный код не был бы корректным. Во-вторых стоит отметить метод component1() для получения значения поля. Генерируется столько componentN() методов, сколько аргументов в конструкторе. Выглядит бесполезно, но на самом деле нужно это для destructuring declaration.

destructuring declaration

Для примера воспользуемся дата классом из предыдущего примера и добавим следующий код:

Kotlin

class DestructuringDeclaration {     fun test() {         val (one, two) = Test("hello", "world")     } }

Decompiled Java

public final class DestructuringDeclaration {    public final void test() {       Test var3 = new Test("hello", "world");       String var1 = var3.component1();       String two = var3.component2();    } }

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

operator

Kotlin

class Something(var likes: Int = 0) {     operator fun inc() = Something(likes+1) }  class Test() {     fun test() {         var something = Something()         something++     } }

Decompiled Java

public final class Something {    private int likes;     @NotNull    public final Something inc() {       return new Something(this.likes + 1);    }     public final int getLikes() {       return this.likes;    }     public final void setLikes(int var1) {       this.likes = var1;    }     public Something(int likes) {       this.likes = likes;    }     // $FF: synthetic method    public Something(int var1, int var2, DefaultConstructorMarker var3) {       if ((var2 & 1) != 0) {          var1 = 0;       }        this(var1);    }     public Something() {       this(0, 1, (DefaultConstructorMarker)null);    } }  public final class Test {    public final void test() {       Something something = new Something(0, 1, (DefaultConstructorMarker)null);       something = something.inc();    } }

Ключевое слово operator нужно для того, чтобы переопределить какой-нибудь оператор языка для конкретного класса. Честно сказать я ни разу не видел чтоб это кто-нибудь использовал, но тем не менее такая возможность есть, а магии внутри нет. По сути компилятор просто подменяет оператор на нужную функцию, примерно также как typealias заменяется на конкретный тип.
И да, если вы прямо сейчас подумали о том, что будет если переопределить оператор идентичности ( === который), то спешу огорчить, это единственный оператор, который переопределить нельзя.

inline class

Kotlin

inline class User(internal val name: String) {     fun upperCase(): String {         return name.toUpperCase()     } }  class Test {     fun test() {         val user = User("Some1")         println(user.upperCase())     } } 

Decompiled Java

public final class Test {    public final void test() {       String user = User.constructor-impl("Some1");       String var2 = User.upperCase-impl(user);       System.out.println(var2);    } }  public final class User {    @NotNull    private final String name;     // $FF: synthetic method    private User(@NotNull String name) {       Intrinsics.checkParameterIsNotNull(name, "name");       super();       this.name = name;    }     @NotNull    public static final String upperCase_impl/* $FF was: upperCase-impl*/(String $this) {       if ($this == null) {          throw new TypeCastException("null cannot be cast to non-null type java.lang.String");       } else {          String var10000 = $this.toUpperCase();          Intrinsics.checkExpressionValueIsNotNull(var10000, "(this as java.lang.String).toUpperCase()");          return var10000;       }    }     @NotNull    public static String constructor_impl/* $FF was: constructor-impl*/(@NotNull String name) {       Intrinsics.checkParameterIsNotNull(name, "name");       return name;    }     // $FF: synthetic method    @NotNull    public static final User box_impl/* $FF was: box-impl*/(@NotNull String v) {       Intrinsics.checkParameterIsNotNull(v, "v");       return new User(v);    }     @NotNull    public static String toString_impl/* $FF was: toString-impl*/(String var0) {       return "User(name=" + var0 + ")";    }     public static int hashCode_impl/* $FF was: hashCode-impl*/(String var0) {       return var0 != null ? var0.hashCode() : 0;    }     public static boolean equals_impl/* $FF was: equals-impl*/(String var0, @Nullable Object var1) {       if (var1 instanceof User) {          String var2 = ((User)var1).unbox-impl();          if (Intrinsics.areEqual(var0, var2)) {             return true;          }       }        return false;    }     public static final boolean equals_impl0/* $FF was: equals-impl0*/(@NotNull String p1, @NotNull String p2) {       Intrinsics.checkParameterIsNotNull(p1, "p1");       Intrinsics.checkParameterIsNotNull(p2, "p2");       throw null;    }     // $FF: synthetic method    @NotNull    public final String unbox_impl/* $FF was: unbox-impl*/() {       return this.name;    }     public String toString() {       return toString-impl(this.name);    }     public int hashCode() {       return hashCode-impl(this.name);    }     public boolean equals(Object var1) {       return equals-impl(this.name, var1);    } }

Из ограничений — можно использовать только один аргумент в конструкторе, впрочем оно и понятно, учитывая что инлайн класс это в целом обертка над какой-то одной переменной. Инлайн класс может содержать в себе методы, но они представляют из себя обычную статику. Также очевидно, что для поддержки интеропа с Java добавлены все необходимые методы.

Итог

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


ссылка на оригинал статьи https://habr.com/post/425077/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *