28 KiB
title, description, tags
| title | description | tags | ||
|---|---|---|---|---|
| Kotlin из Java | Аннотации @JvmStatic, @JvmOverloads, @JvmName, @JvmField, @Throws и то, как конструкции Kotlin выглядят из Java |
|
Обратное направление сложнее: в Kotlin есть конструкции, которых нет в JVM, и компилятор представляет их по-своему. Без аннотаций ваш API окажется неудобным для Java-кода.
class Greeter(val name: String) {
fun greet(greeting: String = "Привет") = "$greeting, $name!"
}
fun main() {
println(Greeter("Мир").greet()) // Привет, Мир!
}
Из 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 |
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
Генерирует перегрузку для каждого значения по умолчанию:
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 невозможно - такой перегрузки нет. Это ограничение влияет на порядок параметров: самые часто переопределяемые ставьте первыми.
Для конструктора аннотация работает так же:
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:
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
Repository.create("1"); // работает: статический метод
Repository.Companion.createNoStatic("2"); // без @JvmStatic только так
То же для object:
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. Три варианта:
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
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:
@file:JvmName("StringUtils")
fun String.slugify() = lowercase().replace(" ", "-")
fun main() {
println("Привет Мир".slugify()) // привет-мир
}
Без аннотации Java-код обращался бы к SlugifyKt.slugify(s), с ней - StringUtils.slugify(s). Расширения становятся статическими методами с получателем первым параметром.
Аксессор свойства:
class Account(private val raw: String) {
@get:JvmName("fetchBalance")
val balance: String get() = raw
}
fun main() {
println(Account("1000").balance) // 1000
}
Из Java метод называется fetchBalance(), а не getBalance().
Обход конфликта стирания. Две функции, различающиеся только параметром дженерика, в байт-коде имеют одинаковую сигнатуру:
@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
@Throws
В Kotlin нет проверяемых исключений, поэтому в байт-коде нет throws:
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
try { loader.load(""); }
catch (IOException e) { } // компилируется
try { loader.loadNoThrows(""); }
catch (IOException e) { } // ОШИБКА: exception is never thrown
Java-компилятор запрещает ловить проверяемое исключение, которое метод не объявляет. Исключение всё равно вылетит в рантайме - просто обработать его можно только через catch (Exception e).
Как выглядят остальные конструкции
Nullability
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-код.
Свойства
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 нет.
Функции-расширения
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-функции
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-клиентов делают обёртку:
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-функции
value class
@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 добавляют обычную перегрузку:
@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
internal
class Service {
internal fun helper() = "вспомогательное"
fun public() = helper()
}
fun main() {
println(Service().public()) // вспомогательное
}
internal компилируется в public с манглированным именем вида helper$main (суффикс - имя модуля). Java-код технически может вызвать helper$main(). Это не изоляция, а соглашение, которое соблюдает только Kotlin-компилятор. Подробнее: Модификаторы видимости
data class
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
sealed
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
Чеклист публичного API для Java-клиентов
@JvmOverloadsна всех функциях со значениями по умолчанию.@JvmStaticна членахcompanion objectиobject.const valили@JvmFieldдля констант.@Throwsтам, где Java-код должен ловить проверяемое исключение.@file:JvmNameдля файлов с функциями верхнего уровня.- Нет
suspendв публичном API - только обёртки (CompletableFuture, колбэки). - Нет
value classв сигнатурах - или перегрузка с@JvmName. - Возвращать
List, а неMutableList, и отдавать копию, если Java может изменить. - Nullable-типы объявлять явно - Java-IDE прочитает аннотации.
- Проверить результат через
javapна реальном классе.
// пример 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 в байт-коде
[!info] Отличия от Java
Java Kotlin из Java staticметод@JvmStaticвcompanion objectpublic 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- настоящий объект, а не пространство имён. В Javastatic- модификатор: метод принадлежит классу. В Kotlin companion - отдельный экземпляр классаCls$Companion, у него есть тип, он может реализовывать интерфейсы и быть получателем расширений. Поэтому без@JvmStaticвызов идёт черезCls.Companion. Подробнее: Object и companion objectОтсутствие проверяемых исключений нельзя «включить» обратно.
@Throwsтолько добавляет запись в атрибутExceptionsметода - сам Kotlin-компилятор её не проверяет. Фактически это документация для Java-компилятора: без неё Java не разрешитcatch, с ней - потребует обработки.
suspendпринципиально несовместим с Java. Корутины реализованы через CPS-преобразование на этапе компиляции Kotlin: функция превращается в конечный автомат с параметромContinuation. Java-компилятор этого преобразования не делает, поэтому написать Java-вызывающий код дляsuspend-функции нельзя - только обёртки. Это самая жёсткая граница interop. Подробнее: Корутины против потоковСравнить: Инкапсуляция в Java, Система исключений в Java
[!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несовместим с половиной модификаторов: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(...)или копию.Платформенные объявления конфликтуют при стирании:
@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не применить. Подробнее: ДелегированиеРасширения не переопределяются и не видны как методы.
fun Foo.bar()из Java - этоFileKt.bar(foo). Java-код не найдётfoo.bar(), а IDE не предложит. Если метод нужен Java-клиентам, делайте его членом класса.
Далее: Kotlin в байт-коде