--- 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["значения по умолчанию
@JvmOverloads"] D --> F["companion / object
@JvmStatic"] D --> G["константы
@JvmField или const"] D --> H["исключения
@Throws"] D --> I["имена
@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() @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.sumAll() = sum() @JvmName("sumDoubles") fun List.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.firstOrDefault(default: String) = firstOrNull() ?: default fun main() { println(listOf("a").firstOrDefault("нет")) // a println(emptyList().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 = 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` из Kotlin в Java имеет `add()`.** Тип стирается до `java.util.List`, и Java-код может вызвать мутирующий метод. Падение будет в рантайме с `UnsupportedOperationException`. Для публичного API возвращайте `Collections.unmodifiableList(...)` или копию. > > **Платформенные объявления конфликтуют при стирании:** > ```kotlin > @JvmName("countStrings") > fun List.count2() = size > > @JvmName("countNumbers") > fun List.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` под именем `x$delegate`. Прямого доступа к значению нет, и `@JvmField` не применить. Подробнее: [Делегирование](../ООП/Делегирование.md) > > **Расширения не переопределяются и не видны как методы.** `fun Foo.bar()` из Java - это `FileKt.bar(foo)`. Java-код не найдёт `foo.bar()`, а IDE не предложит. Если метод нужен Java-клиентам, делайте его членом класса. Далее: [Kotlin в байт-коде](Kotlin%20в%20байт-коде.md)