--- title: Kotlin в байт-коде description: Во что компилируются свойства, лямбды, data class, inline, suspend, value class и null-проверки tags: - kotlin - java-interop - jvm --- Понимание представления Kotlin в байт-коде отвечает на три практических вопроса: почему API выглядит из Java именно так, откуда берутся накладные расходы и что происходит при `NullPointerException` внутри сгенерированного кода. Инструмент один - `javap`: ``` kotlinc Demo.kt -d out # компиляция в .class javap -p -cp out DemoKt # сигнатуры, включая приватные javap -c -p -cp out DemoKt # с байт-кодом javap -v -p -cp out DemoKt # с аннотациями и константным пулом ``` ```kotlin fun add(a: Int, b: Int) = a + b fun main() { println(add(2, 3)) // 5 } ``` Эта функция компилируется в `public static final int add(int, int)` внутри класса `DemoKt`. Никакой обёртки нет - сигнатура такая же, как у Java-метода. ## Где живёт код | Конструкция Kotlin | Класс в байт-коде | |---|---| | `class Foo` | `Foo` | | функции верхнего уровня в `Demo.kt` | `DemoKt` | | `object Bar` | `Bar` с полем `INSTANCE` | | `companion object` в `Foo` | `Foo$Companion` + поле `Foo.Companion` | | вложенный класс | `Foo$Inner` | | анонимный объект | `Foo$method$1` | | лямбда | `invokedynamic`, без отдельного класса | | `enum class Color` | `Color extends java.lang.Enum` | ```kotlin object Registry { private val items = mutableListOf() fun add(item: String) { items += item } fun size() = items.size } fun main() { Registry.add("a") println(Registry.size()) // 1 } ``` `object` становится классом с приватным конструктором, статическим полем `INSTANCE` и статическим инициализатором. Это классический singleton, и его потокобезопасность обеспечена гарантиями JVM на загрузку класса. ## Свойства Свойство - это всегда приватное поле плюс аксессоры: ```kotlin class Holder { var mutable: String = "a" val immutable: Int = 1 private var hidden = 2 lateinit var late: String fun touch() = hidden++ } fun main() { val h = Holder() h.mutable = "b" println("${h.mutable} ${h.immutable} ${h.touch()}") // b 1 2 } ``` В байт-коде: ``` private java.lang.String mutable; private final int immutable; private int hidden; public java.lang.String late; // lateinit — поле публично! public final java.lang.String getMutable(); public final void setMutable(java.lang.String); public final int getImmutable(); public final java.lang.String getLate(); public final void setLate(java.lang.String); ``` Три наблюдения: **`val` даёт `final` поле и только геттер.** Это не константа - значение может вычисляться в конструкторе. **`lateinit var` делает поле публичным.** Компилятор не может сделать его приватным, потому что геттер должен различать «не инициализировано» и «значение `null`» - для этого нужна прямая работа с полем, а `lateinit` исключает nullable-тип. Побочный эффект: поле видно из Java напрямую. **Приватное свойство аксессоров не получает** - доступ идёт к полю, если нет кастомного геттера. Свойство с кастомным геттером поля не создаёт вообще: ```kotlin class Rect(val w: Int, val h: Int) { val area: Int get() = w * h } fun main() { println(Rect(3, 4).area) // 12 } ``` `area` - только метод `getArea()`, считающий значение каждый раз. Подробнее: [Свойства](../ООП/Свойства.md) ## Null-проверки Для каждого ненулевого параметра публичной функции компилятор вставляет проверку: ```kotlin fun nn(s: String) = s.length fun nullable(s: String?) = s?.length fun main() { println(nn("текст")) // 5 println(nullable(null)) // null } ``` Байт-код `nn`: ``` 0: aload_0 1: ldc #9 // String s 3: invokestatic #15 // Intrinsics.checkNotNullParameter:(Object;String)V 6: aload_0 7: invokevirtual #21 // String.length:()I 10: ireturn ``` `checkNotNullParameter` бросает `NullPointerException` с сообщением «Parameter specified as non-null is null» - это и есть защита границы от Java-кода. Для `nullable` проверки нет, зато есть ветвление и упаковка в `Integer`: ``` public static final java.lang.Integer nullable(java.lang.String); ``` Возвращаемый тип `Int?` стал `Integer` - nullable-примитив всегда упакован. Информация о nullability хранится в аннотациях: ``` public static final int nn(java.lang.String); @NotNull на параметре public static final java.lang.Integer nullable(...); @Nullable на методе и параметре ``` Это `org.jetbrains.annotations.@NotNull`/`@Nullable` с `RuntimeInvisible`-удержанием. Их читают Java-IDE и другие JVM-языки. Подробнее: [Null-безопасность](../Синтаксис%20и%20типы/Null-безопасность/Null-безопасность.md) ```mermaid graph LR A["String в Kotlin"] --> B["java.lang.String
+ @NotNull
+ checkNotNullParameter"] C["String? в Kotlin"] --> D["java.lang.String
+ @Nullable
без проверки"] E["Int в Kotlin"] --> F["int"] G["Int? в Kotlin"] --> H["java.lang.Integer"] ``` ## data class ```kotlin data class Point(val x: Int, val y: Int) fun main() { val p = Point(1, 2) println(p) // Point(x=1, y=2) println(p.copy(y = 5)) // Point(x=1, y=5) println(p == Point(1, 2)) // true } ``` Генерируется: ``` private final int x; private final int y; public Point(int, int); public final int getX(); public final int getY(); public final int component1(); public final int component2(); public final Point copy(int, int); public static Point copy$default(Point, int, int, int, java.lang.Object); public java.lang.String toString(); public int hashCode(); public boolean equals(java.lang.Object); ``` `component1()`/`component2()` нужны для деструктуризации - компилятор вызывает их по позиции. `copy$default` реализует значения по умолчанию: четвёртый параметр `int` - битовая маска переданных аргументов, пятый `Object` - технический. Подробнее: [Data class](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Data%20class.md) ## Значения по умолчанию ```kotlin class Service { fun call(url: String, retries: Int = 3, timeout: Long = 1000L) = "$url r=$retries t=$timeout" } fun main() { println(Service().call("/api")) // /api r=3 t=1000 } ``` Два метода вместо одного: ``` public final java.lang.String call(java.lang.String, int, long); public static java.lang.String call$default(Service, java.lang.String, int, long, int, java.lang.Object); ``` `$default` получает битовую маску: бит `i` установлен, если аргумент `i` **не** передан. Внутри он подставляет значение по умолчанию для каждого отсутствующего аргумента и вызывает основной метод. Отсюда два следствия: вызов со значениями по умолчанию стоит на один вызов дороже, а из Java `$default` недоступен (он синтетический и требует знания маски). ## Лямбды и функциональные типы ```kotlin fun noCapture(): () -> Int = { 42 } fun withCapture(n: Int): () -> Int = { n } fun main() { println(noCapture()()) // 42 println(withCapture(7)()) // 7 } ``` Функциональный тип `() -> Int` - это интерфейс `kotlin.jvm.functions.Function0`. Их 23: `Function0` ... `Function22`. ``` public static final kotlin.jvm.functions.Function0 noCapture(); 0: invokedynamic #81, 0 // InvokeDynamic #0:invoke:()Lkotlin/jvm/functions/Function0; ``` Начиная с K2 лямбды компилируются через `invokedynamic` (как в Java), а не в отдельные классы `Foo$lambda$1`. Это уменьшает размер артефакта и ускоряет загрузку, но означает, что имя класса лямбды генерируется в рантайме - в стектрейсах появляются `$$Lambda$...`. Важная деталь: `Function0` работает с **упакованным** `Integer`. Лямбда, возвращающая `Int`, упаковывает результат на каждом вызове - если это горячий цикл, нужна `inline`-функция. Подробнее: [Лямбды и функциональные типы](../Синтаксис%20и%20типы/Работа%20с%20данными/Лямбды%20и%20функциональные%20типы.md) ## inline ```kotlin inline fun measure(block: () -> Unit) { block() } fun useInline() { measure { println("тело") } } fun main() { useInline() // тело } ``` Байт-код `useInline` не содержит вызова `measure` вообще: ``` public static final void useInline(); 0: iconst_0 1: istore_0 2: iconst_0 3: istore_1 4: ldc #49 // String тело 6: getstatic #55 // System.out 9: swap 10: invokevirtual #61 // println 13: nop 14: nop 15: return ``` Тело лямбды подставлено прямо в метод: ни объекта `Function0`, ни вызова по ссылке. Это основа `forEach`, `let`, `map` и остальных операций stdlib - они не создают объектов. Цена - код дублируется в каждой точке вызова. Подробнее: [Inline-функции](../Функциональный%20Kotlin/Inline-функции.md) Сама `measure` всё равно существует как метод (для Java-клиентов и рефлексии): ``` public static final void measure(kotlin.jvm.functions.Function0); ``` ## suspend ```kotlin import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking suspend fun fetch(id: Int): String { delay(10) return "ответ $id" } fun main() = runBlocking { println(fetch(1)) // ответ 1 } ``` Сигнатура меняется радикально: ``` public static final java.lang.Object fetch(int, kotlin.coroutines.Continuation); ``` Три изменения: добавлен параметр `Continuation`, тип возврата стал `Object` (вместо `String`), и само тело превратилось в конечный автомат. Функция может вернуть либо результат, либо специальный маркер `COROUTINE_SUSPENDED`, означающий «я приостановилась, результат придёт через `Continuation`». Конечный автомат - это анонимный класс с полем `label`, хранящим номер точки приостановки, и полями для всех локальных переменных, живущих через приостановку. При возобновлении функция вызывается снова, читает `label` и прыгает в нужную ветку `switch`. Это CPS-преобразование (continuation-passing style), выполняемое компилятором. Отсюда свойства корутин: приостановка не блокирует поток (нет никакого потока - есть объект состояния), стектрейсы короткие, а Java-код вызвать `suspend`-функцию практически не может. Подробнее: [Suspend-функции](../Корутины/Suspend-функции.md) ## value class ```kotlin @JvmInline value class UserId(val value: Int) fun process(id: UserId) = "обработан ${id.value}" fun main() { println(process(UserId(42))) // обработан 42 } ``` Класс `UserId` в байт-коде существует, но в сигнатурах заменяется на обёрнутый тип: ``` public static final java.lang.String process-4ZD5Yi0(int); ``` Параметр стал `int` - обёртки в рантайме нет. Суффикс `-4ZD5Yi0` (хеш от типов) нужен, чтобы `process(UserId)` и `process(OrderId)` не конфликтовали: после стирания у обеих сигнатура `process(int)`. Обёртка всё-таки создаётся в трёх случаях: когда value-класс используется как дженерик-параметр (`List`), как nullable (`UserId?`) и при обращении через супертип. Подробнее: [Value class](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Value%20class.md) ## Делегированные свойства ```kotlin class Config { val expensive: String by lazy { "вычислено" } } fun main() { println(Config().expensive) // вычислено } ``` ``` private final kotlin.Lazy v$delegate; public final java.lang.String getV(); ``` Поле хранит не значение, а делегат. Геттер вызывает `delegate.getValue()`. Поэтому каждое делегированное свойство - это дополнительный объект и один лишний вызов на чтение. Для `by lazy` с дефолтным режимом внутри ещё и `synchronized`-блок при первом доступе. Подробнее: [Делегирование](../ООП/Делегирование.md) ## Дженерики и вариантность ```kotlin class Box>(val value: T) { fun max(other: T) = if (value > other) value else other } fun sizes(l: List, m: MutableList) = l.size + m.size fun main() { println(Box(3).max(7)) // 7 println(sizes(listOf(1, 2), mutableListOf(3))) // 3 } ``` ``` public final class Box> public static final int sizes(java.util.List, java.util.List); ``` Проекции Kotlin становятся wildcards Java: `out T` → `? extends T`, `in T` → `? super T`. Дженерики стираются так же, как в Java - в рантайме `Box` и `Box` неразличимы. Обойти это позволяет только `reified` в `inline`-функциях, где тип подставляется при компиляции. Подробнее: [Вариантность](../Дженерики/Вариантность.md), [Reified и inline](../Дженерики/Reified%20и%20inline.md) ## Nothing и Unit ```kotlin fun fail(): Nothing = throw IllegalStateException("всегда падает") fun act(): Unit = println("действие") fun main() { act() // действие println(runCatching { fail() }.isFailure) // true } ``` `Unit` как возвращаемый тип становится `void`. `Nothing` - `java.lang.Void` (значение которого всегда `null`, потому что функция никогда не возвращается нормально). В обоих случаях Kotlin-компилятор знает больше, чем байт-код: `Nothing` делает код после вызова недостижимым, но Java-компилятор этого не увидит. Подробнее: [Any, Unit, Nothing](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Any,%20Unit,%20Nothing.md) ## Метаданные Каждый Kotlin-класс несёт аннотацию `kotlin.Metadata` с закодированной информацией о nullable-типах, значениях по умолчанию, свойствах, `suspend`, вариантности, `inline`, `sealed` и прочем. Именно её читает Kotlin-компилятор, подключая скомпилированную библиотеку: `.class`-файла недостаточно, чтобы восстановить Kotlin-API. Практические следствия: **Обфускация ломает Kotlin-interop.** ProGuard/R8 должны сохранять `kotlin.Metadata`, иначе библиотека перестанет быть пригодной для Kotlin-клиентов (но останется пригодной для Java). **Рефлексия Kotlin требует `kotlin-reflect`.** `KClass.memberProperties` читает метаданные, и для этого нужна отдельная зависимость весом ~2 МБ. Java-рефлексия (`javaClass.declaredFields`) работает без неё, но видит только байт-код. Подробнее: [Рефлексия](../Рефлексия/Рефлексия.md) **Java-библиотека не знает про Kotlin-типы.** Jackson, Hibernate и подобные видят только `.class`: конструктор со значениями по умолчанию, nullable-поле как обычное. Для корректной работы нужны модули-адаптеры (`jackson-module-kotlin`), которые читают `kotlin.Metadata`. ## Сводная таблица | Kotlin | Байт-код | |---|---| | `fun f()` на верхнем уровне | `static final` в `ФайлKt` | | `object O` | класс + `static final O INSTANCE` | | `companion object` | класс `Cls$Companion` + поле | | `val x: T` | `private final T x` + `getX()` | | `var x: T` | `private T x` + `getX()`/`setX()` | | `lateinit var x` | `public T x` (поле публично) | | `val x get() = ...` | только `getX()`, поля нет | | `x: String` (параметр) | `String` + `@NotNull` + `checkNotNullParameter` | | `x: String?` | `String` + `@Nullable` | | `Int` | `int` | | `Int?` | `java.lang.Integer` | | `() -> Int` | `Function0` | | лямбда | `invokedynamic` | | `inline fun` | тело подставлено + метод для Java | | `suspend fun f(): T` | `Object f(Continuation)` + автомат | | `@JvmInline value class` | обёрнутый тип + суффикс в имени | | `by lazy` | поле `x$delegate: Lazy` | | `out T` / `in T` | `? extends T` / `? super T` | | `Unit` | `void` | | `Nothing` | `java.lang.Void` | | `internal fun f()` | `public final f$module()` | > [!info] Отличия от Java > | Java | Kotlin | > |---|---| > | исходник ≈ байт-код | есть зазор, есть метаданные | > | поля публичны, если так написано | поля всегда приватны (кроме `lateinit`) | > | геттер пишется руками | генерируется | > | лямбда → `invokedynamic` | то же (с K2) | > | нет значений по умолчанию | синтетический `$default` | > | нет `inline` | тело подставляется | > | `async` через библиотеку | `suspend` → конечный автомат | > | `int` и `Integer` в коде | `Int` и `Int?` | > > **Главное отличие - наличие метаданных.** В Java `.class` полностью описывает API: что видно в сигнатурах, то и есть. В Kotlin часть системы типов (nullability, значения по умолчанию, `suspend`, `inline`, `sealed`, вариантность) существует только в аннотации `kotlin.Metadata`. Это значит, что Kotlin-компилятор и Java-компилятор видят **разные** API одной библиотеки: первый богаче. Отсюда и необходимость сохранять метаданные при обфускации, и зависимость `kotlin-reflect`, и адаптеры для Jackson. > > **Проверки на null стоят вызова метода на каждый параметр.** В Java такой проверки нет вообще - `NullPointerException` возникает при первом использовании. Kotlin переносит сбой на границу функции, что делает диагностику проще (понятно, какой параметр виноват), но добавляет `invokestatic` на каждый ненулевой параметр публичной функции. В приватных и `inline`-функциях проверок нет: там компилятор уже гарантировал тип. > > **`inline` - возможность, которой в Java нет на уровне языка.** JIT в Java тоже встраивает вызовы, но это решение рантайма, непредсказуемое и не применимое к лямбдам с захватом. Kotlin встраивает на этапе компиляции, гарантированно и вместе с телом лямбды. Это и позволяет `collection.map { }` не создавать объектов, и даёт `reified`, и делает возможными `return` из лямбды. Цена - рост байт-кода и невозможность изменить тело без перекомпиляции клиентов. > > **`suspend` реализован компилятором, а не рантаймом.** В Java асинхронность требует либо потоков, либо `CompletableFuture` с цепочками колбэков, либо виртуальных потоков (Loom), которые меняют JVM. Kotlin делает CPS-преобразование на этапе компиляции и работает на любой JVM 8+. Следствие: корутины не требуют поддержки платформы, но и не интегрируются с Java-кодом. > > **Value-классы ближе к будущим Project Valhalla, чем к Java сегодня.** В Java любая обёртка над `int` - это объект в куче. Kotlin убирает обёртку на этапе компиляции, заменяя тип в сигнатурах. Цена - манглирование имён и обёртка в дженериках, где стирание не позволяет обойтись примитивом. > > Сравнить: [JVM](../../Программирование%20на%20Java/JVM/JVM.md), [Байт-код](../../Программирование%20на%20Java/JVM/Байт-код.md) > [!warning] Подводные камни > **`lateinit var` создаёт публичное поле.** Это нарушает инкапсуляцию неожиданным образом: Java-код и рефлексия получают прямой доступ в обход геттера, минуя проверку инициализации. Если это важно, используйте `var x: T? = null` с приватным сеттером. > > **Nullable-примитив всегда упакован:** > ```kotlin > fun main() { > val plain: Int = 1 > val nullable: Int? = 1 > println(plain == nullable) // true > println(nullable!!::class.java.name) // java.lang.Integer > val a: Int? = 1000 > val b: Int? = 1000 > println("identity: ${a === b}") // identity: false > } > ``` > `Int?` это `Integer`, поэтому `===` на значениях вне кеша `-128..127` даёт `false`. В горячем коде `Int?` в коллекциях и лямбдах создаёт объекты. Подробнее: [Числа и переполнение](../Синтаксис%20и%20типы/Работа%20с%20данными/Числа%20и%20переполнение.md) > > **Значения по умолчанию стоят лишнего вызова.** `call("/api")` идёт через `call$default`, который разбирает битовую маску и вызывает `call`. Для горячего пути с миллионами вызовов разница измерима - там пишут явные перегрузки. > > **`inline` раздувает байт-код.** Большая `inline`-функция, вызванная в сотне мест, копируется сто раз. Компилятор предупреждает про «expected performance impact... insignificant» для функций без лямбда-параметров - это намёк, что `inline` там не нужен. > > **Изменение тела `inline`-функции требует перекомпиляции клиентов.** Старое тело уже вкомпилировано в их байт-код. Для публичных библиотек это серьёзное ABI-ограничение: `inline` в публичном API фиксирует реализацию. > > **Делегированное свойство - это объект и вызов.** `by lazy` на каждом из сотни свойств даёт сотню объектов `Lazy` и сотню косвенных вызовов. Для простых случаев дешевле `private var x: T? = null` с проверкой в геттере. > > **Стектрейс корутины не показывает путь вызова.** Конечный автомат возобновляется из диспетчера, поэтому в стектрейсе видна только текущая точка приостановки. Для отладки включают `-Dkotlinx.coroutines.debug` или зависимость `kotlinx-coroutines-debug`. Подробнее: [Корутины](../Корутины/Корутины.md) > > **Обфускация без правил для `kotlin.Metadata` ломает библиотеку для Kotlin-клиентов.** Код продолжит работать, но исчезнет nullability, пропадут значения по умолчанию, `kotlin-reflect` перестанет видеть свойства. В правилах R8 нужно `-keep class kotlin.Metadata`. > > **`javap` не показывает Kotlin-API.** Он читает байт-код, не метаданные: `suspend`-функция выглядит как метод с `Continuation`, а `data class` - как обычный класс с кучей методов. Чтобы увидеть Kotlin-вид скомпилированного кода, нужен `kotlinp` или IDE («Show Kotlin Bytecode» → «Decompile»). > > **Синтетические методы видны рефлексии.** `component1()`, `copy$default`, `access$...`, `Foo$Companion` - всё это попадёт в `getDeclaredMethods()`. Код, перебирающий методы класса (сериализаторы, валидаторы), должен фильтровать по `isSynthetic()`. > > **`internal` не защищает.** Манглированное имя `helper$module` публично. Java-код в том же артефакте вызовет его без препятствий, как и любая рефлексия. > > **Проверки `checkNotNullParameter` можно отключить, но не стоит.** Флаг `-Xno-param-assertions` убирает их, давая небольшой выигрыш. Цена - `NullPointerException` будет возникать глубоко внутри вместо границы функции, и диагностика станет заметно хуже. Далее: [Инструментарий](../Инструментарий/Инструментарий.md)