Kotlin guide done. (20.10.26)

This commit is contained in:
2026-10-20 12:54:25 +03:00
parent da3ad9dd1a
commit 6853ada348
81 changed files with 30112 additions and 0 deletions
@@ -0,0 +1,385 @@
---
title: Java из Kotlin
description: Платформенные типы, синтетические свойства, SAM-конверсия, массивы, статические члены и escape-хатчи
tags:
- kotlin
- java-interop
---
Вызов Java из Kotlin почти не требует усилий: Java-классы видны как обычные Kotlin-классы. Трение возникает в четырёх местах - null-безопасность, свойства, массивы и ключевые слова.
```kotlin
import java.io.File
import java.util.ArrayList
fun main() {
val list = ArrayList<String>()
list.add("первый")
list.add("второй")
println(list.size) // 2
println(list.joinToString()) // первый, второй
val tmp = File.createTempFile("demo", ".txt")
tmp.writeText("содержимое")
println(tmp.readText()) // содержимое
tmp.delete()
}
```
Обратите внимание: `joinToString` и `writeText` - это Kotlin-расширения, работающие на Java-типах. Stdlib добавляет сотни таких функций к `java.util` и `java.io`. Подробнее: [Функции-расширения](../Функциональный%20Kotlin/Функции-расширения.md)
## Платформенные типы
Главная особенность: тип из Java записывается как `String!` - компилятор не знает, может ли там быть `null`.
```kotlin
fun main() {
// System.getProperty возвращает String! — может быть null
val asNonNull: String = System.getProperty("java.version") // компилируется
val asNullable: String? = System.getProperty("нет.такого") // тоже компилируется
println(asNonNull.isNotEmpty()) // true
println(asNullable?.length) // null
}
```
Компилятор разрешает оба варианта, поэтому ответственность на вас. Если Java вернёт `null` там, где объявлен ненулевой тип, проверка сработает в точке присваивания:
```kotlin
fun main() {
val error = runCatching {
val value: String = System.getProperty("отсутствует")
value.length
}.exceptionOrNull()
println(error?.javaClass?.simpleName) // NullPointerException
}
```
Сообщение будет вида «Parameter specified as non-null is null» или «null cannot be cast to non-null type» - проверка вставляется компилятором. Подробнее: [Платформенные типы](../Синтаксис%20и%20типы/Null-безопасность/Платформенные%20типы.md)
**Аннотации в Java решают проблему.** Если Java-код аннотирован, платформенных типов не возникает:
```java
// Java
@Nullable public String findName() { return null; } // в Kotlin: String?
@NotNull public String getId() { return "1"; } // в Kotlin: String
public String plain() { return null; } // в Kotlin: String!
```
Поддерживаются `org.jetbrains.annotations`, JSR-305 (`javax.annotation`), `androidx.annotation`, Lombok, Eclipse и Checker Framework. Это единственный способ получить настоящую null-безопасность на границе с Java, поэтому в смешанных проектах аннотировать Java-код стоит.
## Синтетические свойства
Java-геттеры и сеттеры видны как свойства Kotlin:
```kotlin
import java.util.Calendar
fun main() {
val cal = Calendar.getInstance()
cal.timeInMillis = 0L // вместо setTimeInMillis(0)
println(cal.timeInMillis) // 0
val sb = StringBuilder("текст")
println(sb.length) // 5 — вместо length()
}
```
Правила распознавания:
| Java-метод | Свойство в Kotlin |
|---|---|
| `getValue()` | `value` (только чтение) |
| `getValue()` + `setValue(T)` | `value` (чтение и запись) |
| `isReady()` | `isReady` (имя сохраняется) |
| `setOnly(T)` без геттера | свойства нет, только метод |
| `getValue(int index)` | свойства нет - есть параметр |
```kotlin
fun main() {
val thread = Thread.currentThread()
println(thread.name.isNotEmpty()) // true — getName()
println(thread.isDaemon) // false — isDaemon() сохраняет префикс
println(thread.priority) // 5 — getPriority()
}
```
Синтетические свойства доступны только для Java-методов. Kotlin-функция с именем `getFoo()` свойством не становится.
## SAM-конверсия
Java-интерфейс с одним абстрактным методом принимает лямбду:
```kotlin
import java.util.concurrent.Executors
fun main() {
val executor = Executors.newSingleThreadExecutor()
executor.submit { println("как Runnable") } // лямбда -> Runnable
val future = executor.submit<String> { "как Callable" } // лямбда -> Callable
println(future.get())
val items = mutableListOf("гамма", "альфа", "бета")
items.sortWith { a, b -> a.compareTo(b) } // Comparator
println(items)
executor.shutdown()
// как Runnable
// как Callable
// [альфа, бета, гамма]
}
```
Когда перегрузок несколько и тип неоднозначен, интерфейс указывают явно:
```kotlin
import java.util.concurrent.Executors
fun main() {
val executor = Executors.newSingleThreadExecutor()
val task = Runnable { println("явная SAM-конструкция") }
executor.submit(task)
executor.shutdown()
Thread.sleep(50)
// явная SAM-конструкция
}
```
Важная тонкость: SAM-конверсия работает для **Java**-интерфейсов автоматически. Для Kotlin-интерфейса нужно объявить его как `fun interface`:
```kotlin
fun interface Validator {
fun validate(input: String): Boolean
}
fun check(v: Validator, s: String) = v.validate(s)
fun main() {
println(check({ it.isNotEmpty() }, "текст")) // true
}
```
Подробнее: [Лямбды и функциональные типы](../Синтаксис%20и%20типы/Работа%20с%20данными/Лямбды%20и%20функциональные%20типы.md)
## Массивы
Java-массивы отображаются на типизированные классы Kotlin, причём примитивные - на специальные:
```kotlin
fun main() {
val ints: IntArray = intArrayOf(1, 2, 3) // int[]
val strings: Array<String> = arrayOf("a", "b") // String[]
val boxed: Array<Int> = arrayOf(1, 2) // Integer[]
println(ints::class.simpleName) // IntArray
println(strings::class.simpleName) // Array
println(ints.sum()) // 6
println(strings.joinToString()) // a, b
println(boxed.size) // 2
}
```
`IntArray` компилируется в `int[]` без упаковки, а `Array<Int>` - в `Integer[]`. Это важно для производительности и для совместимости: Java-метод, принимающий `int[]`, не примет `Array<Int>`.
Передача массива в varargs требует spread-оператора:
```kotlin
fun main() {
val parts = arrayOf("a", "b", "c")
println(String.format("%s-%s-%s", *parts)) // a-b-c
val numbers = intArrayOf(3, 1, 2)
println(listOf(*numbers.toTypedArray()).sorted()) // [1, 2, 3]
}
```
`*` распаковывает массив в отдельные аргументы - без него массив передался бы как один элемент.
Статические методы класса `java.lang.String` через Kotlin-овский `String` недоступны - `String` в Kotlin это `kotlin.String`, у него нет `join`:
```kotlin
fun main() {
val parts = listOf("x", "y")
println(java.lang.String.join("/", parts)) // x/y
println(parts.joinToString("/")) // x/y — идиоматично
}
```
## Статические члены
```kotlin
fun main() {
println(Integer.MAX_VALUE) // 2147483647
println(Integer.parseInt("42")) // 42
println(Math.max(3, 7)) // 7
val list = java.util.Collections.emptyList<String>()
println(list.size) // 0
}
```
Статические методы и поля вызываются через имя класса, как в Java. Для часто используемых есть Kotlin-аналоги: `"42".toInt()` вместо `Integer.parseInt`, `maxOf(3, 7)` вместо `Math.max`, `emptyList()` вместо `Collections.emptyList()`.
## Java-ключевые слова как идентификаторы
Если Java-метод называется как ключевое слово Kotlin, его имя берётся в обратные кавычки:
```kotlin
class JavaLike {
fun `is`(value: Int) = value > 0 // в Kotlin `is` — ключевое слово
fun `in`(value: Int) = value
fun `object`() = "объект"
}
fun main() {
val j = JavaLike()
println(j.`is`(5)) // true
println(j.`in`(1)) // 1
println(j.`object`()) // объект
}
```
Чаще всего это встречается с методами `is`, `in`, `object`, `when`, `val`, `var`, `fun`, `typealias`. Backticks работают и для имён с пробелами - этим пользуются в названиях тестов. Подробнее: [kotlin.test и JUnit 5](../Тестирование/kotlin.test%20и%20JUnit%205.md)
## Checked exceptions исчезают
```kotlin
import java.io.File
fun main() {
// Java-метод объявляет throws IOException, но Kotlin этого не требует
val tmp = File.createTempFile("demo", ".txt")
tmp.writeText("данные")
println(tmp.readText())
tmp.delete()
// данные
}
```
В Kotlin нет проверяемых исключений: `try/catch` вокруг `createTempFile` не обязателен. Это упрощает код, но требует дисциплины - компилятор больше не напоминает об обработке ошибок. Подробнее: [Система исключений](../Исключения/Система%20исключений.md)
## Наследование от Java-класса
```kotlin
import java.io.IOException
class CustomException(message: String, cause: Throwable? = null) :
IOException(message, cause)
abstract class BaseTask : Runnable {
abstract fun execute()
override fun run() {
try {
execute()
} catch (e: Exception) {
println("ошибка: ${e.message}")
}
}
}
class FailingTask : BaseTask() {
override fun execute() = throw CustomException("задача упала")
}
fun main() {
FailingTask().run() // ошибка: задача упала
}
```
Наследование работает в обе стороны без ограничений: Kotlin-класс может расширять Java-класс, реализовывать Java-интерфейс и быть предком Java-класса.
## Java-типы, которые заменяют на Kotlin-аналоги
| Java | Предпочтительно в Kotlin | Почему |
|---|---|---|
| `java.util.ArrayList` | `mutableListOf()` | короче, тот же класс внутри |
| `Integer.parseInt(s)` | `s.toInt()` | расширение, читается слева направо |
| `String.valueOf(x)` | `x.toString()` | то же |
| `Math.max(a, b)` | `maxOf(a, b)` | работает с любым `Comparable` |
| `Collections.sort(list)` | `list.sort()` | расширение |
| `Optional<T>` | `T?` | nullable-тип нативен |
| `new String[]{...}` | `arrayOf(...)` | короче |
| `StringBuilder` | интерполяция или `buildString` | читаемее |
```kotlin
fun main() {
val fromJava = java.util.ArrayList<String>().apply { add("a") }
val idiomatic = mutableListOf("a")
println("тот же класс: ${fromJava::class == idiomatic::class}") // тот же класс: true
println(buildString {
append("собрано ")
append("через buildString")
})
// собрано через buildString
}
```
`mutableListOf()` возвращает именно `java.util.ArrayList` - разница только в синтаксисе.
## Optional
```kotlin
import java.util.Optional
fun main() {
val present: Optional<String> = Optional.of("значение")
val empty: Optional<String> = Optional.empty()
println(present.orElse("по умолчанию")) // значение
println(empty.orElse("по умолчанию")) // по умолчанию
// перевод в идиоматичный Kotlin
val asNullable: String? = present.orElse(null)
println(asNullable?.length) // 8
println(empty.getOrNull()?.length) // null
}
fun <T> Optional<T>.getOrNull(): T? = orElse(null)
```
`Optional` в Kotlin-коде избыточен: его роль выполняет `T?`, который не создаёт обёртку и поддержан синтаксисом (`?.`, `?:`, `let`). При получении `Optional` из Java-API его сразу переводят в nullable.
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | `getName()` | `name` (синтетическое свойство) |
> | `isReady()` | `isReady` |
> | `new Runnable() { ... }` | `{ ... }` (SAM) |
> | `int[]` | `IntArray` |
> | `Integer[]` | `Array<Int>` |
> | `method(array)` для varargs | `method(*array)` |
> | `throws IOException` обязателен | не требуется |
> | `Optional<T>` | `T?` |
>
> **Платформенные типы - компромисс, которого в Java нет.** В чистом Kotlin компилятор гарантирует отсутствие NPE. На границе с Java гарантия исчезает: тип `String!` означает «компилятор не знает». Альтернативой было бы считать все Java-типы nullable - тогда Java-код требовал бы `?.` и `!!` на каждом шагу и стал бы неудобен. Авторы Kotlin выбрали практичность: ответственность за null на границе лежит на программисте.
>
> **Синтетические свойства работают только для Java.** Это важная асимметрия: Java-метод `getName()` в Kotlin доступен и как `getName()`, и как `.name`, а Kotlin-функция `getName()` - только как функция. Причина в том, что в Kotlin для этого есть настоящие свойства, и превращать функции в свойства означало бы двусмысленность.
>
> **SAM-конверсия автоматическая только для Java-интерфейсов.** В Java любой интерфейс с одним методом принимает лямбду. В Kotlin - нет: нужен `fun interface`, иначе требуется `object : Interface { }`. Причина - в Kotlin есть функциональные типы (`(String) -> Boolean`), которые покрывают большинство случаев, и SAM-интерфейсы нужны реже.
>
> **Массивы различаются сильнее, чем кажется.** В Java `int[]` и `Integer[]` - разные типы, но оба «массивы». В Kotlin это `IntArray` и `Array<Int>`, причём `Array<Int>` ковариантен по-другому: `Array<String>` не является `Array<Any>`, тогда как в Java `String[]` является `Object[]` - что и породило `ArrayStoreException`. Kotlin эту дыру закрыл, сделав `Array<T>` инвариантным. Подробнее: [Вариантность](../Дженерики/Вариантность.md)
>
> **Отсутствие проверяемых исключений - сознательное решение.** Опыт Java показал, что `throws` чаще приводит к `catch (Exception e) { }` и обёрткам в `RuntimeException`, чем к осмысленной обработке, а в сочетании с лямбдами и дженериками создаёт неразрешимые проблемы (функциональный интерфейс не может объявить произвольный `throws`). Цена - потеря документирующей роли и необходимость `@Throws` при вызове из Java.
>
> Сравнить: [Массивы в Java](../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Массивы.md), [Система исключений в Java](../../Программирование%20на%20Java/Исключения/Система%20исключений.md)
> [!warning] Подводные камни
> **Платформенный тип даёт NPE в неожиданном месте:**
> ```kotlin
> fun main() {
> val props = System.getProperties()
> val error = runCatching {
> val missing: String = props.getProperty("нет.такого")
> missing.uppercase()
> }.exceptionOrNull()
> println("NPE при присваивании: ${error is NullPointerException}")
> // NPE при присваивании: true
> }
> ```
> Исключение возникает не там, где значение используется, а там, где оно присваивается ненулевому типу. Стектрейс указывает на строку присваивания, а не на Java-метод.
>
> **`!!` на платформенном типе бессмысленен.** `javaMethod()!!` не добавляет безопасности - проверка и так вставляется. Правильно - `?:` с осмысленным значением по умолчанию или явный тип `String?`.
>
> **Java-аннотации nullability обязательны для настоящей безопасности.** Без них компилятор Kotlin не знает ничего, и весь Java-API остаётся зоной риска. Это основная причина добавлять `@Nullable`/`@NotNull` в унаследованный Java-код при миграции.
>
> **Неизменяемая Kotlin-коллекция, переданная в Java, может быть изменена:**
> ```kotlin
> fun main() {
> val readOnly = listOf("a")
> val error = runCatching { (readOnly as MutableList<String>).add("b") }
> .exceptionOrNull()
> println(error?.javaClass?.simpleName) // UnsupportedOperationException
> }
> ```
> Java-код увидит `java.util.List` с методом `add()` и может его вызвать. Приведение компилируется, падение - в рантайме. Передавайте копию через `toMutableList()`, если Java-код изменяет коллекцию.
>
> **`Array<Int>` и `IntArray` не взаимозаменяемы.** Java-метод с параметром `int[]` требует `IntArray`; `arrayOf(1, 2)` даёт `Array<Int>` (то есть `Integer[]`) и не подойдёт. Конвертация - `toIntArray()`, и она копирует данные.
>
> **Забытый spread-оператор меняет смысл:**
> ```kotlin
> fun main() {
> val parts = arrayOf("a", "b")
> println(listOf(parts).size) // 1 — массив как один элемент
> println(listOf(*parts).size) // 2 — распакованный
> }
> ```
> Компилятор не всегда предупреждает - `listOf(parts)` корректен, просто создаёт `List<Array<String>>`.
>
> **`getClass()` вызывается как `javaClass`.** Метод `Object.getClass()` в Kotlin недоступен напрямую: он конфликтует с `::class`. Используйте `javaClass` для `java.lang.Class` и `::class` для `KClass`.
>
> **Статические вложенные Java-классы требуют полного пути.** `Map.Entry` в Kotlin - это `Map.Entry`, но `java.util.AbstractMap.SimpleEntry` нужно импортировать явно; обращение через внешний класс (`AbstractMap.SimpleEntry`) работает только после импорта внешнего.
>
> **Lombok-геттеры видны, но не всегда.** Lombok генерирует методы на этапе аннотационной обработки Java, и Kotlin-компилятор, работающий первым, их не видит. Нужен плагин `kotlin("plugin.lombok")` и `lombok.config` с `lombok.addLombokGeneratedAnnotation = true`.
>
> **Java-дженерики с wildcards переводятся в проекции.** `List<? extends Number>` становится `List<out Number>`, `List<? super Integer>` - `List<in Integer>`. Raw-тип `List` без параметра становится `List<*>`, и элементы из него имеют тип `Any?`. Подробнее: [Проекции типов](../Дженерики/Проекции%20типов.md)
>
> **Вызов Java-метода с `Object`-параметром принимает всё.** `method(Object o)` в Kotlin имеет параметр `Any!`, поэтому туда пройдёт и `null`, и любое значение - компилятор не поможет. Для таких API особенно важны аннотации.
>
> **SAM-конверсия не работает для абстрактных классов.** `Thread(Runnable { })` - да, но `object : Thread() { }` для переопределения `run()` - только через наследование. SAM применяется только к интерфейсам с единственным абстрактным методом.
Далее: [Kotlin из Java](Kotlin%20из%20Java.md)
@@ -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)
@@ -0,0 +1,525 @@
---
title: Kotlin из Java
description: Аннотации @JvmStatic, @JvmOverloads, @JvmName, @JvmField, @Throws и то, как конструкции Kotlin выглядят из Java
tags:
- kotlin
- java-interop
---
Обратное направление сложнее: в Kotlin есть конструкции, которых нет в JVM, и компилятор представляет их по-своему. Без аннотаций ваш API окажется неудобным для Java-кода.
```kotlin
class Greeter(val name: String) {
fun greet(greeting: String = "Привет") = "$greeting, $name!"
}
fun main() {
println(Greeter("Мир").greet()) // Привет, Мир!
}
```
Из Java этот же класс выглядит так:
```java
// Java
Greeter g = new Greeter("Мир");
System.out.println(g.greet("Привет")); // параметр обязателен!
// g.greet(); // не скомпилируется
```
Значение по умолчанию существует только для Kotlin: в байт-коде есть `greet(String)` и синтетический `greet$default(Greeter, String, int, Object)`, вызывать который вручную нельзя.
## Карта проблем и аннотаций
| Конструкция Kotlin | Из Java без аннотации | Аннотация |
|---|---|---|
| значение по умолчанию | один метод со всеми параметрами | `@JvmOverloads` |
| `companion object` | `Cls.Companion.method()` | `@JvmStatic` |
| `object` | `Singleton.INSTANCE.method()` | `@JvmStatic` |
| `val` в `companion object` | `Cls.Companion.getX()` | `@JvmField` |
| `const val` | уже поле - ничего не нужно | - |
| функция верхнего уровня | `FileNameKt.fn()` | `@JvmName` на файле |
| свойство | `getX()` / `setX()` | `@get:JvmName` |
| `suspend fun` | лишний параметр `Continuation` | не решается |
| `value class` | манглированное имя | не решается |
| исключение | не видно в `throws` | `@Throws` |
| перегрузки с дженериками | конфликт стирания | `@JvmName` |
```mermaid
graph TD
A["Kotlin API"] --> B{"Вызывается из Java?"}
B -->|"нет"| C["аннотации не нужны"]
B -->|"да"| D["проверить каждый пункт"]
D --> E["значения по умолчанию<br/>@JvmOverloads"]
D --> F["companion / object<br/>@JvmStatic"]
D --> G["константы<br/>@JvmField или const"]
D --> H["исключения<br/>@Throws"]
D --> I["имена<br/>@JvmName"]
```
## @JvmOverloads
Генерирует перегрузку для каждого значения по умолчанию:
```kotlin
class Greeter(val name: String) {
@JvmOverloads
fun greet(greeting: String = "Привет", punct: String = "!") = "$greeting, $name$punct"
}
fun main() {
val g = Greeter("Мир")
println(g.greet()) // Привет, Мир!
println(g.greet("Здравствуй")) // Здравствуй, Мир!
println(g.greet("Hi", ".")) // Hi, Мир.
}
```
В байт-коде появляются три метода:
```
public final String greet(String, String) // полный
public final String greet(String) // punct по умолчанию
public final String greet() // оба по умолчанию
```
Перегрузки создаются **справа налево**: параметры отбрасываются с конца. Поэтому `greet(punct = ".")` из Java невозможно - такой перегрузки нет. Это ограничение влияет на порядок параметров: самые часто переопределяемые ставьте первыми.
Для конструктора аннотация работает так же:
```kotlin
class Config @JvmOverloads constructor(
val host: String = "localhost",
val port: Int = 8080,
) {
override fun toString() = "$host:$port"
}
fun main() {
println(Config()) // localhost:8080
println(Config("example.com")) // example.com:8080
println(Config("example.com", 443)) // example.com:443
}
```
## @JvmStatic
Без неё члены `companion object` доступны только через `.Companion`:
```kotlin
class Repository private constructor(val id: String) {
companion object {
@JvmStatic
fun create(id: String) = Repository(id)
fun createNoStatic(id: String) = Repository(id)
}
}
fun main() {
println(Repository.create("1").id) // 1
println(Repository.createNoStatic("2").id) // 2
}
```
Из Kotlin обе формы одинаковы. Из Java - нет:
```java
// Java
Repository.create("1"); // работает: статический метод
Repository.Companion.createNoStatic("2"); // без @JvmStatic только так
```
То же для `object`:
```kotlin
object Registry {
private val items = mutableListOf<String>()
@JvmStatic
fun register(item: String) { items += item }
fun size() = items.size
}
fun main() {
Registry.register("a")
println(Registry.size()) // 1
}
```
Из Java: `Registry.register("a")` против `Registry.INSTANCE.size()`.
## @JvmField и const
Свойство в `companion object` компилируется в приватное поле с геттером на `Companion`. Три варианта:
```kotlin
class Limits {
companion object {
const val MAX = 100 // примитив/String, compile-time
@JvmField val DEFAULT = "значение" // поле, доступно напрямую
val COMPUTED = "вычисляется".uppercase() // только через геттер
}
}
fun main() {
println(Limits.MAX) // 100
println(Limits.DEFAULT) // значение
println(Limits.COMPUTED) // ВЫЧИСЛЯЕТСЯ
}
```
Из Java:
```java
// Java
Limits.MAX; // public static final int
Limits.DEFAULT; // public static final String
Limits.Companion.getCOMPUTED(); // только геттер
```
`const val` требует примитив или `String` и инициализацию константным выражением - зато встраивается в место вызова (и потому меняет ABI при изменении значения). `@JvmField` работает с любым типом, но несовместим с `private`, `open`, `override`, `lateinit` и делегированными свойствами.
## @JvmName
Задаёт имя в байт-коде. Три точки применения.
**Файл.** Функции верхнего уровня попадают в класс `ИмяФайлаKt`:
```kotlin
@file:JvmName("StringUtils")
fun String.slugify() = lowercase().replace(" ", "-")
fun main() {
println("Привет Мир".slugify()) // привет-мир
}
```
Без аннотации Java-код обращался бы к `SlugifyKt.slugify(s)`, с ней - `StringUtils.slugify(s)`. Расширения становятся статическими методами с получателем первым параметром.
**Аксессор свойства:**
```kotlin
class Account(private val raw: String) {
@get:JvmName("fetchBalance")
val balance: String get() = raw
}
fun main() {
println(Account("1000").balance) // 1000
}
```
Из Java метод называется `fetchBalance()`, а не `getBalance()`.
**Обход конфликта стирания.** Две функции, различающиеся только параметром дженерика, в байт-коде имеют одинаковую сигнатуру:
```kotlin
@JvmName("sumInts")
fun List<Int>.sumAll() = sum()
@JvmName("sumDoubles")
fun List<Double>.sumAll() = sum()
fun main() {
println(listOf(1, 2, 3).sumAll()) // 6
println(listOf(1.5, 2.5).sumAll()) // 4.0
}
```
Без `@JvmName` компилятор откажется: «platform declaration clash». Это одна из немногих ситуаций, когда аннотация нужна даже для чисто Kotlin-кода. Подробнее: [Reified и inline](../Дженерики/Reified%20и%20inline.md)
## @Throws
В Kotlin нет проверяемых исключений, поэтому в байт-коде нет `throws`:
```kotlin
import java.io.IOException
class Loader {
@Throws(IOException::class)
fun load(path: String): String {
if (path.isEmpty()) throw IOException("пустой путь")
return "данные из $path"
}
fun loadNoThrows(path: String): String {
if (path.isEmpty()) throw IOException("пустой путь")
return "данные из $path"
}
}
fun main() {
val loader = Loader()
println(loader.load("/tmp/file")) // данные из /tmp/file
println(runCatching { loader.loadNoThrows("") }.isFailure) // true
}
```
Из Java разница принципиальна:
```java
// Java
try { loader.load(""); }
catch (IOException e) { } // компилируется
try { loader.loadNoThrows(""); }
catch (IOException e) { } // ОШИБКА: exception is never thrown
```
Java-компилятор запрещает ловить проверяемое исключение, которое метод не объявляет. Исключение всё равно вылетит в рантайме - просто обработать его можно только через `catch (Exception e)`.
## Как выглядят остальные конструкции
### Nullability
```kotlin
class Validator {
fun check(input: String): Boolean = input.isNotEmpty()
fun find(key: String): String? = if (key == "a") "найдено" else null
}
fun main() {
val v = Validator()
println(v.check("текст")) // true
println(v.find("b")) // null
}
```
`check(String)` получает аннотацию `@NotNull` на параметре и проверку `Intrinsics.checkNotNullParameter` в начале тела. Java-код, передавший `null`, получит `NullPointerException` с сообщением «Parameter specified as non-null is null» - сразу, а не при первом использовании. `find` возвращает `@Nullable String`, и IDE предупредит Java-код.
### Свойства
```kotlin
class User {
var name: String = "аноним"
val id: Int = 1
var internalState: String = "скрыто"
private set
}
fun main() {
val u = User()
u.name = "Иван"
println("${u.name} ${u.id} ${u.internalState}") // Иван 1 скрыто
}
```
Из Java: `getName()`/`setName()`, `getId()` без сеттера, `getInternalState()` без сеттера (приватный сеттер в байт-коде приватен). Само поле всегда приватно - прямого доступа из Java нет.
### Функции-расширения
```kotlin
fun List<String>.firstOrDefault(default: String) = firstOrNull() ?: default
fun main() {
println(listOf("a").firstOrDefault("нет")) // a
println(emptyList<String>().firstOrDefault("нет")) // нет
}
```
Из Java это статический метод с получателем первым аргументом: `FileNameKt.firstOrDefault(list, "нет")`. Никакого синтаксического сахара - расширение существует только в Kotlin.
### 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
}
```
В байт-коде сигнатура становится `Object fetch(int, Continuation)`, возвращая либо результат, либо маркер `COROUTINE_SUSPENDED`. Вызвать такое из Java практически невозможно. Для Java-клиентов делают обёртку:
```kotlin
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.future.future
import java.util.concurrent.CompletableFuture
class Api(private val scope: CoroutineScope) {
suspend fun fetch(id: Int): String {
delay(10)
return "ответ для $id"
}
// для Java: CompletableFuture вместо suspend
fun fetchAsync(id: Int): CompletableFuture<String> =
scope.future { fetch(id) }
}
fun main() {
val scope = CoroutineScope(Dispatchers.Default)
println(Api(scope).fetchAsync(7).get()) // ответ для 7
}
```
Нужна зависимость `kotlinx-coroutines-jdk8`. Подробнее: [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
}
```
В байт-коде `process` получает имя `process-4ZD5Yi0(int)` - суффикс нужен, чтобы отличать перегрузки с разными value-классами над одним примитивом. Вызвать такое имя из Java нельзя: дефис недопустим в идентификаторе. Для Java-API добавляют обычную перегрузку:
```kotlin
@JvmInline
value class Money(val cents: Long)
fun charge(amount: Money) = "списано ${amount.cents}"
@JvmName("chargeCents")
fun charge(cents: Long) = charge(Money(cents))
fun main() {
println(charge(Money(500))) // списано 500
println(charge(500L)) // списано 500
}
```
Подробнее: [Value class](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Value%20class.md)
### internal
```kotlin
class Service {
internal fun helper() = "вспомогательное"
fun public() = helper()
}
fun main() {
println(Service().public()) // вспомогательное
}
```
`internal` компилируется в `public` с манглированным именем вида `helper$main` (суффикс - имя модуля). Java-код технически может вызвать `helper$main()`. Это не изоляция, а соглашение, которое соблюдает только Kotlin-компилятор. Подробнее: [Модификаторы видимости](../ООП/Модификаторы%20видимости.md)
### 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)
val (x, y) = p
println("$x $y") // 1 2
}
```
Из Java доступны `getX()`, `getY()`, `equals`, `hashCode`, `toString`, `component1()`, `component2()` и `copy(int, int)`. Но `copy` с одним аргументом недоступна - это те же значения по умолчанию, и `@JvmOverloads` на сгенерированном `copy` поставить нельзя. Деструктуризации в Java нет. Подробнее: [Data class](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Data%20class.md)
### sealed
```kotlin
sealed interface Result
data class Ok(val value: String) : Result
data class Err(val message: String) : Result
fun describe(r: Result) = when (r) {
is Ok -> "успех: ${r.value}"
is Err -> "ошибка: ${r.message}"
}
fun main() {
println(describe(Ok("данные"))) // успех: данные
println(describe(Err("сбой"))) // ошибка: сбой
}
```
Из Java `sealed interface` Kotlin - обычный интерфейс (при JVM-таргете 17+ может компилироваться в настоящий `sealed`). Исчерпывающего `switch` Java не получит - гарантия полноты обеспечивается только Kotlin-компилятором. Подробнее: [Sealed class и interface](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Sealed%20class%20и%20interface.md)
## Чеклист публичного API для Java-клиентов
1. `@JvmOverloads` на всех функциях со значениями по умолчанию.
2. `@JvmStatic` на членах `companion object` и `object`.
3. `const val` или `@JvmField` для констант.
4. `@Throws` там, где Java-код должен ловить проверяемое исключение.
5. `@file:JvmName` для файлов с функциями верхнего уровня.
6. Нет `suspend` в публичном API - только обёртки (`CompletableFuture`, колбэки).
7. Нет `value class` в сигнатурах - или перегрузка с `@JvmName`.
8. Возвращать `List`, а не `MutableList`, и отдавать копию, если Java может изменить.
9. Nullable-типы объявлять явно - Java-IDE прочитает аннотации.
10. Проверить результат через `javap` на реальном классе.
```kotlin
// пример API, готового к вызову из Java
@file:JvmName("UserApi")
class UserService @JvmOverloads constructor(
private val baseUrl: String = "https://api.example.com",
) {
@Throws(IllegalArgumentException::class)
@JvmOverloads
fun find(id: String, includeDeleted: Boolean = false): String {
require(id.isNotBlank()) { "id не может быть пустым" }
return "$baseUrl/users/$id?deleted=$includeDeleted"
}
companion object {
const val DEFAULT_TIMEOUT_MS = 5_000L
@JvmStatic
fun default() = UserService()
}
}
fun main() {
println(UserService.default().find("42"))
println(UserService.DEFAULT_TIMEOUT_MS)
// https://api.example.com/users/42?deleted=false
// 5000
}
```
## Проверка результата
Единственный надёжный способ увидеть реальный API - посмотреть байт-код:
```
kotlinc Api.kt -d out
javap -cp out com.example.UserApi
javap -cp out 'com.example.UserService$Companion'
```
`javap` покажет точные сигнатуры, включая синтетические методы `$default` и манглированные имена. Это стоит делать перед релизом библиотеки. Подробнее: [Kotlin в байт-коде](Kotlin%20в%20байт-коде.md)
> [!info] Отличия от Java
> | Java | Kotlin из Java |
> |---|---|
> | `static` метод | `@JvmStatic` в `companion object` |
> | `public static final` поле | `const val` или `@JvmField` |
> | перегрузки пишутся руками | `@JvmOverloads` генерирует |
> | `throws` в сигнатуре | `@Throws` |
> | имя класса = имя файла | `@file:JvmName` |
> | утилитный класс со `static` | функции верхнего уровня |
> | геттер пишется руками | генерируется из свойства |
>
> **Аннотации `@Jvm*` - это не костыли, а явное управление ABI.** В Java представление в байт-коде однозначно: что написано, то и скомпилируется. В Kotlin между исходником и байт-кодом есть зазор, и аннотации дают над ним контроль. Плата за выразительность языка: `@JvmOverloads` существует потому, что в JVM нет значений по умолчанию, а не из-за недоработки.
>
> **Значения по умолчанию в Kotlin устроены иначе, чем перегрузки в Java.** В Java N необязательных параметров требуют N перегрузок, каждую с делегированием. В Kotlin - один метод и синтетический `$default`, принимающий битовую маску переданных аргументов. Отсюда и ограничение `@JvmOverloads`: перегрузки генерируются только отбрасыванием с конца, потому что произвольные комбинации дали бы экспоненциальное число методов.
>
> **`companion object` - настоящий объект, а не пространство имён.** В Java `static` - модификатор: метод принадлежит классу. В Kotlin companion - отдельный экземпляр класса `Cls$Companion`, у него есть тип, он может реализовывать интерфейсы и быть получателем расширений. Поэтому без `@JvmStatic` вызов идёт через `Cls.Companion`. Подробнее: [Object и companion object](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Object%20и%20companion%20object.md)
>
> **Отсутствие проверяемых исключений нельзя «включить» обратно.** `@Throws` только добавляет запись в атрибут `Exceptions` метода - сам Kotlin-компилятор её не проверяет. Фактически это документация для Java-компилятора: без неё Java не разрешит `catch`, с ней - потребует обработки.
>
> **`suspend` принципиально несовместим с Java.** Корутины реализованы через CPS-преобразование на этапе компиляции Kotlin: функция превращается в конечный автомат с параметром `Continuation`. Java-компилятор этого преобразования не делает, поэтому написать Java-вызывающий код для `suspend`-функции нельзя - только обёртки. Это самая жёсткая граница interop. Подробнее: [Корутины против потоков](../Корутины/Корутины%20против%20потоков.md)
>
> Сравнить: [Инкапсуляция в Java](../../Программирование%20на%20Java/ООП/Инкапсуляция/Инкапсуляция.md), [Система исключений в Java](../../Программирование%20на%20Java/Исключения/Система%20исключений.md)
> [!warning] Подводные камни
> **`@JvmOverloads` генерирует перегрузки только справа налево.** Для `fun f(a: Int = 1, b: Int = 2, c: Int = 3)` появятся `f(a,b,c)`, `f(a,b)`, `f(a)`, `f()` - но не `f(c)` и не `f(a,c)`. Из Java нельзя передать только третий параметр. Ставьте чаще переопределяемые параметры раньше.
>
> **Добавление `@JvmStatic` или `@JvmOverloads` позже ломает совместимость.** `@JvmStatic` не удаляет метод с `Companion`, но меняет рекомендуемую точку входа; снятие аннотации удаляет статический метод и роняет скомпилированный Java-код с `NoSuchMethodError`. Решайте до релиза.
>
> **Изменение `const val` требует перекомпиляции клиентов.** Значение встраивается в место вызова, поэтому Java-код, собранный против старой версии, продолжит использовать старое число. Для значений, которые могут меняться, лучше `@JvmField val`.
>
> **`@JvmField` несовместим с половиной модификаторов:**
> ```kotlin
> class Limits {
> companion object {
> @JvmField val OK = "работает"
> // @JvmField private val BAD = "" // ошибка: не может быть private
> // @JvmField lateinit var LATE: String // ошибка: не для lateinit
> }
> }
>
> fun main() {
> println(Limits.OK) // работает
> }
> ```
> Запрещено для `private`, `open`, `override`, `const`, `lateinit` и делегированных свойств.
>
> **`copy()` у `data class` из Java требует все аргументы.** `@JvmOverloads` на сгенерированный `copy` поставить нельзя. Если Java-коду нужна частичная копия, пишите свой метод или билдер.
>
> **Манглированные имена value-классов недоступны из Java.** `fun takeId(id: UserId)` компилируется в `takeId-4ZD5Yi0(int)`; дефис делает имя невызываемым в Java. Это проявится только при попытке интеграции - Kotlin-компилятор не предупредит.
>
> **`internal` виден из Java.** `internalFun$main()` - публичный метод в байт-коде. Если нужна настоящая закрытость для Java-кода в том же проекте, используйте `private` или выносите в отдельный модуль.
>
> **`List<T>` из Kotlin в Java имеет `add()`.** Тип стирается до `java.util.List`, и Java-код может вызвать мутирующий метод. Падение будет в рантайме с `UnsupportedOperationException`. Для публичного API возвращайте `Collections.unmodifiableList(...)` или копию.
>
> **Платформенные объявления конфликтуют при стирании:**
> ```kotlin
> @JvmName("countStrings")
> fun List<String>.count2() = size
>
> @JvmName("countNumbers")
> fun List<Int>.count2() = size
>
> fun main() {
> println(listOf("a").count2()) // 1
> println(listOf(1, 2).count2()) // 2
> }
> ```
> Без `@JvmName` будет ошибка компиляции «platform declaration clash» - обе функции дают `count2(List)`. Это проявляется и в чисто Kotlin-проекте.
>
> **`@file:JvmName` должна стоять до `package`.** Файловые аннотации идут самой первой строкой файла, иначе компилятор их не примет.
>
> **`@get:JvmName` на свойстве в интерфейсе или `open` классе запрещена.** Переименование аксессора сломало бы переопределение, поэтому компилятор его не допускает для `open`/`override` членов.
>
> **Делегированные свойства из Java выглядят как обычные.** `by lazy` даёт `getX()`, но поле хранит `Lazy<T>` под именем `x$delegate`. Прямого доступа к значению нет, и `@JvmField` не применить. Подробнее: [Делегирование](../ООП/Делегирование.md)
>
> **Расширения не переопределяются и не видны как методы.** `fun Foo.bar()` из Java - это `FileKt.bar(foo)`. Java-код не найдёт `foo.bar()`, а IDE не предложит. Если метод нужен Java-клиентам, делайте его членом класса.
Далее: [Kotlin в байт-коде](Kotlin%20в%20байт-коде.md)
@@ -0,0 +1,202 @@
---
title: Взаимодействие с Java
description: Зачем нужен interop, что работает без усилий, где возникает трение и как устроен раздел
tags:
- kotlin
- java-interop
---
Kotlin проектировался как язык, который должен без потерь жить в существующей Java-экосистеме. Это не побочная возможность, а требование: без доступа к Spring, Jackson, JDBC и остальной инфраструктуре JVM язык был бы бесполезен.
```kotlin
import java.time.LocalDate
import java.util.UUID
fun main() {
val id = UUID.randomUUID()
val today = LocalDate.of(2026, 1, 15)
println(id.toString().length) // 36
println(today.plusDays(30)) // 2026-02-14
println(today.dayOfWeek) // THURSDAY
}
```
Это обычный Java API, вызванный как родной Kotlin-код: никаких обёрток, конвертеров и объявлений внешних типов.
## Что происходит без усилий
| Что | Как работает |
|---|---|
| вызов Java-метода из Kotlin | как обычный метод |
| геттеры/сеттеры Java | как свойства Kotlin |
| Java-коллекции | как `MutableList`, `MutableMap` и т.д. |
| наследование от Java-класса | как от Kotlin-класса |
| реализация Java-интерфейса | SAM-конверсия из лямбды |
| Java-аннотации | применяются к Kotlin-коду |
| смешанная компиляция в одном модуле | поддерживается Gradle-плагином |
```kotlin
import java.util.concurrent.Executors
fun main() {
val executor = Executors.newSingleThreadExecutor()
executor.submit { println("Runnable из лямбды") } // SAM-конверсия
executor.shutdown()
val list = java.util.ArrayList<String>()
list.add("элемент") // как MutableList
println(list.size) // 1
val builder = StringBuilder()
builder.append("a").append("b")
println(builder.length) // 2
// Runnable из лямбды
}
```
## Где возникает трение
Три системы типов Kotlin не имеют соответствия в Java, и именно на этих границах появляются сложности.
**1. Null-безопасность.** Java не различает `String` и `String?`, поэтому любое значение из Java - платформенный тип `String!`:
```kotlin
fun main() {
// Java-метод вернул null, а Kotlin считал тип ненулевым
val value: String = System.getProperty("несуществующее.свойство")
?: "значение по умолчанию"
println(value) // значение по умолчанию
}
```
Без `?:` здесь был бы `NullPointerException` в момент присваивания. Подробнее: [Платформенные типы](../Синтаксис%20и%20типы/Null-безопасность/Платформенные%20типы.md)
**2. Изменяемость коллекций.** В Kotlin `List` и `MutableList` - разные типы, в Java это один `java.util.List`:
```kotlin
fun main() {
val immutable: List<String> = listOf("a", "b")
// передача в Java-метод, который может вызвать add()
val asJava: java.util.List<String> = immutable as java.util.List<String>
val error = runCatching { asJava.add("c") }.exceptionOrNull()
println("неизменяемый список отверг изменение: ${error != null}") // неизменяемый список отверг изменение: true
}
```
Java-код не видит разницы и может попытаться изменить список, который Kotlin считает неизменяемым. Подробнее: [Изменяемость](../Коллекции/Изменяемость.md)
**3. Возможности Kotlin, которых нет в Java.** Значения по умолчанию, функции-расширения, `suspend`, value-классы, `object` - всё это компилируется в конструкции JVM, но из Java выглядит иначе, чем задумано. Для управления этим есть аннотации `@JvmStatic`, `@JvmOverloads`, `@JvmName` и другие. Подробнее: [Kotlin из Java](Kotlin%20из%20Java.md)
## Асимметрия направлений
Направления не равноценны - и это главное, что нужно понять про interop.
```mermaid
graph LR
A["Java-код"] -->|"почти без трения:<br/>только null-безопасность"| B["Kotlin-код"]
B -->|"требует аннотаций:<br/>@JvmStatic, @JvmOverloads,<br/>@JvmName, @JvmField"| A
```
**Java из Kotlin** работает почти прозрачно: язык создавался именно для этого. Единственная серьёзная проблема - платформенные типы, то есть риск `NullPointerException` там, где компилятор уверен в ненулевом значении.
**Kotlin из Java** требует внимания: без аннотаций ваш API окажется неудобным - значения по умолчанию превратятся в один метод с лишними параметрами, `companion object` потребует обращения через `.Companion`, а `suspend`-функция получит параметр `Continuation`.
Практический вывод: если ваш Kotlin-код вызывается из Java, его публичный API нужно проектировать осознанно. Если Kotlin-код вызывается только из Kotlin, аннотации не нужны вообще.
## Карта раздела
| Заметка | О чём |
|---|---|
| [Java из Kotlin](Java%20из%20Kotlin.md) | платформенные типы, SAM, массивы, статика, синтетические свойства |
| [Kotlin из Java](Kotlin%20из%20Java.md) | `@JvmStatic`, `@JvmOverloads`, `@JvmName`, `@JvmField`, `@Throws` |
| [Kotlin в байт-коде](Kotlin%20в%20байт-коде.md) | во что компилируются свойства, лямбды, `data class`, `suspend` |
## Смешанный проект
Gradle-плагин Kotlin компилирует Java и Kotlin вместе, разрешая циклические зависимости между языками:
```kotlin
// build.gradle.kts
plugins {
kotlin("jvm") version "2.4.0"
java
}
kotlin {
jvmToolchain(21)
}
// Kotlin и Java в одном модуле:
// src/main/kotlin/ и src/main/java/ — компилируются совместно
```
Порядок компиляции: сначала Kotlin видит Java-исходники (через стабы), затем `javac` компилирует Java, уже видя готовые Kotlin-классы. Поэтому Java-класс может наследоваться от Kotlin-класса, который реализует Java-интерфейс. Подробнее: [Структура проекта](../Инструментарий/Структура%20проекта.md)
## Стратегия миграции
Типичный путь для существующего Java-проекта:
1. **Новый код на Kotlin, старый не трогать.** Interop позволяет это без прослоек.
2. **Конвертировать по файлу**, начиная с листовых классов (DTO, утилиты) - у них меньше зависимостей.
3. **Проверять результат автоконвертера.** IDE конвертирует Java в Kotlin, но результат получается «Java, написанная на Kotlin»: платформенные типы вместо nullable, `!!` вместо `?.`, классы вместо `data class`.
4. **Публичный API, вызываемый из Java, аннотировать** сразу - иначе придётся менять его потом.
5. **Тесты конвертировать последними или не конвертировать** - они и так работают.
```kotlin
// типичный результат автоконвертера — работает, но это не идиоматичный Kotlin
class UserDtoConverted {
var name: String? = null
var age: Int? = null
fun getDisplayName(): String {
return if (name != null) name!! else "аноним"
}
}
// идиоматичная версия
data class UserDto(val name: String?, val age: Int?) {
val displayName: String get() = name ?: "аноним"
}
fun main() {
println(UserDtoConverted().getDisplayName()) // аноним
println(UserDto(null, null).displayName) // аноним
}
```
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | один язык, одна система типов | две системы, граница между ними |
> | `String` может быть `null` | `String` и `String?` различаются |
> | `List` один тип | `List` и `MutableList` |
> | `int` и `Integer` различаются | `Int` и `Int?` |
> | статические методы | `companion object` + `@JvmStatic` |
> | нет аналога | платформенные типы `String!` |
> | checked exceptions обязательны | проверяемых исключений нет |
>
> **Interop работает, потому что Kotlin компилируется в тот же байт-код.** Kotlin-класс - это обычный класс JVM: у него те же модификаторы доступа, то же наследование, тот же формат `.class`. Никакой прослойки, маршалинга или рефлексии в runtime нет, поэтому вызов Kotlin-метода из Java стоит ровно столько же, сколько вызов Java-метода. Это принципиально отличает ситуацию от interop между языками разных платформ (JNI, JNA), где границу приходится пересекать с накладными расходами.
>
> **Цена - необходимость представлять конструкции Kotlin средствами JVM.** В байт-коде нет ни nullable-типов, ни значений по умолчанию, ни функций-расширений. Kotlin эмулирует их: nullable-информацию хранит в аннотациях и метаданных, значения по умолчанию - в синтетическом методе `greet$default`, расширения - в статических методах. Из Kotlin это видно как родные конструкции (компилятор читает метаданные), из Java - как реализация. Подробнее: [Kotlin в байт-коде](Kotlin%20в%20байт-коде.md)
>
> **Проверяемые исключения - односторонняя потеря.** В Kotlin их нет: `throws IOException` из Java-метода не обязывает ни к `try/catch`, ни к объявлению. Это упрощает Kotlin-код, но Java-код, вызывающий Kotlin-метод, не узнает о возможном исключении без `@Throws`. Подробнее: [Система исключений](../Исключения/Система%20исключений.md)
>
> **Null-безопасность Kotlin не распространяется на Java-код.** Компилятор не может проверить то, чего нет в типах Java. Поэтому аннотации `@Nullable`/`@NotNull` в Java-коде - не документация, а реальная информация для компилятора Kotlin: с ними платформенные типы становятся обычными nullable/non-null.
>
> Сравнить: [JVM](../../Программирование%20на%20Java/JVM/JVM.md), [Байт-код](../../Программирование%20на%20Java/JVM/Байт-код.md)
> [!warning] Подводные камни
> **Платформенные типы отключают проверки компилятора.** Значение из Java имеет тип `String!`, который компилятор позволяет использовать и как `String`, и как `String?`. Ошибка проявится как `NullPointerException` в рантайме, в точке присваивания:
> ```kotlin
> fun main() {
> val error = runCatching {
> val value: String = System.getProperty("нет.такого") // вернёт null
> value.length
> }.exceptionOrNull()
> println("NPE от платформенного типа: ${error is NullPointerException}")
> // NPE от платформенного типа: true
> }
> ```
>
> **Автоконвертер даёт работающий, но плохой код.** Типичные следы: `!!` вместо `?.`, `var x: T? = null` вместо конструктора, геттеры вместо свойств, обычные классы вместо `data class`. После конвертации файл нужно переписывать вручную, иначе Kotlin-проект унаследует все недостатки Java-кода и не получит преимуществ.
>
> **Java-код может нарушить неизменяемость Kotlin-коллекции.** `List<String>` из Kotlin приходит в Java как `java.util.List`, у которого есть `add()`. Вызов упадёт с `UnsupportedOperationException` в рантайме, а компилятор об этом не предупредит. Передавайте в Java копию (`toList()`) или `Collections.unmodifiableList`.
>
> **Kotlin-код без аннотаций неудобен из Java.** Значения по умолчанию, `companion object`, `object`, `suspend`-функции и функции верхнего уровня выглядят из Java не так, как задумано. Если API публичный, это нужно решать до релиза - добавление `@JvmStatic` позже меняет сигнатуры и ломает совместимость.
>
> **`internal` в Kotlin из Java доступен.** Он компилируется в `public` с манглированным именем (`internalFun$main`), поэтому Java-код в том же проекте технически может его вызвать. Это не защита, а соглашение.
>
> **Проверяемые исключения из Kotlin не видны Java.** Kotlin-метод, бросающий `IOException`, из Java выглядит как не бросающий ничего - `catch (IOException e)` вокруг него не скомпилируется («exception is never thrown»). Нужна аннотация `@Throws`.
>
> **Смешанная компиляция требует правильного плагина.** В Gradle нужен `kotlin("jvm")` и, при наличии Java-исходников, корректно настроенный `sourceSets`. Без этого Java-код не увидит Kotlin-классы, а ошибка выглядит как «cannot find symbol».
>
> **Generics Java стираются и в Kotlin.** `List<String>` и `List<Int>` в рантайме неразличимы по обе стороны границы, поэтому проверка типа элементов коллекции из Java невозможна. Подробнее: [Reified и inline](../Дженерики/Reified%20и%20inline.md)
Далее: [Java из Kotlin](Java%20из%20Kotlin.md)