Files
obsidian/Kotlin/Взаимодействие с Java/Kotlin в байт-коде.md
T

435 lines
29 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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<String>()
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<br/>+ @NotNull<br/>+ checkNotNullParameter"]
C["String? в Kotlin"] --> D["java.lang.String<br/>+ @Nullable<br/>без проверки"]
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<Integer>`. Их 23: `Function0` ... `Function22`.
```
public static final kotlin.jvm.functions.Function0<java.lang.Integer> noCapture();
0: invokedynamic #81, 0 // InvokeDynamic #0:invoke:()Lkotlin/jvm/functions/Function0;
```
Начиная с K2 лямбды компилируются через `invokedynamic` (как в Java), а не в отдельные классы `Foo$lambda$1`. Это уменьшает размер артефакта и ускоряет загрузку, но означает, что имя класса лямбды генерируется в рантайме - в стектрейсах появляются `$$Lambda$...`.
Важная деталь: `Function0<Integer>` работает с **упакованным** `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<kotlin.Unit>);
```
## 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<? super java.lang.String>);
```
Три изменения: добавлен параметр `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<UserId>`), как 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<T : Comparable<T>>(val value: T) {
fun max(other: T) = if (value > other) value else other
}
fun sizes(l: List<out Number>, m: MutableList<in Int>) = l.size + m.size
fun main() {
println(Box(3).max(7)) // 7
println(sizes(listOf(1, 2), mutableListOf(3))) // 3
}
```
```
public final class Box<T extends java.lang.Comparable<? super T>>
public static final int sizes(java.util.List<? extends java.lang.Number>, java.util.List<? super java.lang.Integer>);
```
Проекции Kotlin становятся wildcards Java: `out T` → `? extends T`, `in T` → `? super T`. Дженерики стираются так же, как в Java - в рантайме `Box<Int>` и `Box<String>` неразличимы. Обойти это позволяет только `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<Integer>` |
| лямбда | `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)