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

526 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: Kotlin из 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)