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

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

Я спеціально втрачу досить побиті і відомі моменти так як, напевно, немає сенсу в сотий раз писати про генерації геттеров/сеттерів для var і подібних речах. Отже почнемо.

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

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

Далі у вікні просто натискаємо 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.

Читайте також  Full disclosure: 0day-уразливість втечі з VirtualBox

Приблизно на цьому пізнання 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 функціях, які як вже зазначалося просто копіюються, вставляються в потрібні місця, таким чином вже під час виклику функції компілятор вже в курсі що саме там за тип і може модифікувати байткод.

Читайте також  Chrome Audit на 500: Частина 1. Лендінг

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

Може все-таки 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();
}
}

Зазвичай ця можливість припадає пилом на полиці, але іноді може бути корисною, наприклад, при роботі з вмістом мап.

Читайте також  DevBoy: робимо генератор сигналів

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 вельми цікава і може багато чого прояснити.

Степан Лютий

Обожнюю технології в сучасному світі. Хоча частенько і замислююся над тим, як далеко вони нас заведуть. Не те, щоб я прям і знаюся на ядрах, пікселях, коллайдерах і інших парсеках. Просто приходжу в захват від того, що може в творчому пориві вигадати людський розум.

Вам також сподобається...

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *