Kotlin guide done. (20.10.26)
This commit is contained in:
@@ -0,0 +1,434 @@
|
||||
---
|
||||
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)
|
||||
Reference in New Issue
Block a user