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,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)