1. Как Java видит Kotlin на JVM
Сегодня логично перевернуть стрелку: не «Kotlin вызывает Java», а «Java вызывает Kotlin». Мы не будем писать Java‑код как отдельную практику, но разберём, как Kotlin‑код увидит Java, и научимся двумя маленькими аннотациями делать Kotlin‑API существенно удобнее для Java‑потребителя.
Зачем вообще думать о Java‑представлении
Представьте, что вы написали аккуратный Kotlin‑код: функции уровня файла, константы, форматирование отчётов. А потом приходит коллега (или вы сами через полгода) и говорит: «А можно я это вызову из Java?». И тут начинается маленькая драма: Java не знает про многие Kotlin‑удобства и видит ваш код через призму JVM. Поэтому важно понимать, какие имена и формы получаются на байткоде, и где можно чуть‑чуть «подсластить» interop.
В нашей консольной программе (пусть это будет мини‑трекер трат) уже есть много утилитных функций: парсинг команд, форматирование строк, подготовка отчётов. Это отличный кандидат на «маленькое API», которым теоретически может пользоваться и Java‑код (например, другой модуль вашей компании, который до Kotlin ещё не дошёл морально — и это нормально).
Почему Java видит странный SomethingKt
До этого момента мы спокойно писали функции «просто в файле»:
fun formatMoney(amount: Int): String = "$amount ₣ "
С точки зрения Kotlin это естественно: файл — нормальное место для функций‑утилит. Но на JVM всё устроено так, что функции должны «жить» в каком‑то классе. Поэтому компилятор делает для вас технический класс‑обёртку: file facade (его ещё часто называют «классом файла»).
Если ваш файл называется ReportUtils.kt, то Java обычно увидит что-то вроде ReportUtilsKt и статические методы внутри него. То есть Java‑вызов (условно) выглядел бы как ReportUtilsKt.formatMoney(10).
Это не «плохо» и не «ошибка». Это просто реальность JVM: ей нужно куда-то положить статические методы, и она кладёт их в класс, имя которого вы иногда даже не планировали «экспортировать в люди».
2. Человеческое имя для класса файла: @file:JvmName
Когда вы делаете библиотеку, утилитный модуль или просто код, которым кто-то будет пользоваться извне, внезапно оказывается важным, как выглядит имя в Java. Называть API‑точку входа ReportUtilsKt — терпимо, но слегка «пахнет компилятором». Kotlin позволяет это исправить одной аннотацией: @file:JvmName("...").
Ключевая идея: мы управляем тем, как JVM‑класс файла будет называться для внешнего мира. При этом Kotlin‑код внутри проекта обычно вообще не страдает: в Kotlin вы по-прежнему вызываете функции по их обычным именам.
Важно и то, где писать file‑аннотации. По соглашениям оформления Kotlin‑кода file‑аннотации размещаются после комментария файла (если он есть) и до package, и отделяются пустой строкой, чтобы было визуально очевидно, что аннотация относится именно к файлу, а не к пакету.
Пример файла «экспортного API»
Допустим, мы хотим собрать в одном месте функции, которые формируют строки отчёта. Создадим файл ExpenseApi.kt:
@file:JvmName("ExpenseApi")
package app.expenses
fun formatMoney(amount: Int): String {
return "$amount ₣ "
}
fun formatExpenseLine(title: String, amount: Int): String {
return "$title: ${formatMoney(amount)}"
}
Обратите внимание на две вещи.
Во-первых, @file:JvmName("ExpenseApi") стоит в самом верху и задаёт JVM‑имя класса файла. Именно такой стиль размещения file‑аннотаций считается правильным и ожидаемым.
Во-вторых, мы не создавали никаких классов руками. Мы всё ещё в «до‑ООП» части курса: только функции и коллекции.
Как это выглядит на уровне идеи
Если представить, как Java будет видеть этот файл, идея такая:
flowchart TD
A["ExpenseApi.kt top-level fun"] --> B["JVM class (facade)"]
B --> C["Java calls static methods"]
B["JVM class name = ExpenseApi"] --> C
Без @file:JvmName JVM‑класс почти наверняка назывался бы ExpenseApiKt (потому что файл ExpenseApi.kt). С аннотацией он становится просто ExpenseApi.
И да, это ровно тот случай, когда «мы не пишем Java», но делаем так, чтобы Java‑коллеге не пришлось каждый день видеть суффикс Kt и вспоминать, что это вообще такое.
3. Когда свойство становится полем: @JvmField
Почему val обычно не «поле»
Когда вы пишете:
val appName: String = "Budget CLI"
в Kotlin это выглядит как «переменная/константа». Но на JVM свойство обычно превращается в:
1) поле (где хранится значение),
2) метод‑геттер getAppName() (а для var ещё и сеттер setAppName(...)).
То есть Java по умолчанию взаимодействует со свойством через методы, а не через прямой доступ к полю. Это хорошо сочетается с инкапсуляцией и тем, что Kotlin‑свойства могут иметь кастомные геттеры/сеттеры.
И вот тут возникает interop‑нюанс: если вы хотите, чтобы Java видела ваше значение как «простое поле» (например, как константу или как публичный флаг), то доступ через getX() может казаться излишне многословным, а иногда ещё и мешает стилю кода.
@JvmField и границы применимости
Иногда вы правда хотите: «пусть это будет публичное поле, без геттеров и сеттеров». Для этого существует аннотация @JvmField.
Её смысл: экспортировать Kotlin‑свойство как JVM‑поле, чтобы Java могла обращаться к нему напрямую, без getX().
Важно сразу зафиксировать границу: @JvmField имеет смысл в основном ради Java‑потребителя. В Kotlin‑коде вы всё равно пишете API_VERSION, DEFAULT_CURRENCY, и для Kotlin‑разработчика почти ничего не меняется. Но для Java меняется форма доступа.
Пример: версия API и «баннер» приложения
Добавим в пакет app.expenses файл InteropConstants.kt:
package app.expenses
import kotlin.jvm.JvmField
@JvmField
val API_VERSION: Int = 1
val banner: String
get() = "Expense CLI (api=$API_VERSION)"
Здесь специально два разных случая.
API_VERSION — это хранимое значение, у него есть backing field (то есть реальное место хранения). Его можно экспортировать как поле.
banner — вычисляемое свойство: оно каждый раз «собирается» геттером. У него нет отдельного поля хранения, значит «экспортировать как поле» просто нечего. Это важная идея: @JvmField работает только там, где реально есть поле.
Как Java будет это видеть
| Kotlin объявление | Что обычно генерируется на JVM | Что будет при @JvmField |
|---|---|---|
|
поле + getX() | публичное поле X (геттера нет) |
|
поле + getX() + setX(...) | публичное поле X (без аксессоров) |
|
только getX() | нельзя (нет backing field) |
Это не «договорённость на словах», а прямое следствие того, как Kotlin кладёт свойства на JVM.
4. Мини‑встраивание в CLI‑проект: Java‑friendly API без магии
Сейчас будет важный момент. Мы не будем превращать наш проект в библиотеку мирового уровня, не будем обсуждать Gradle‑публикации и прочие взрослые слова. Но мы сделаем так, чтобы код выглядел аккуратно и «экспортопригодно»: отдельный файл API, понятное JVM‑имя и пара значений, которые можно читать как поля.
Пусть наш проект — это консольный трекер трат, который хранит расходы в списке MutableList<Triple<String, Int, String>>, где лежит (категория, сумма, комментарий).
Файл с API‑функциями: @file:JvmName
Создадим ExpenseInterop.kt:
@file:JvmName("ExpenseInterop")
package app.expenses
fun makeExpense(category: String, amount: Int, note: String): Triple<String, Int, String> {
return Triple(category, amount, note)
}
fun expenseAmount(expense: Triple<String, Int, String>): Int {
return expense.second
}
Да, это простые функции, и Kotlin‑разработчик мог бы написать expense.second прямо везде. Но смысл «API‑файла» в том, что вы формируете поверхность, которой могут пользоваться другие языки и модули.
И да, file‑аннотацию мы ставим именно наверху файла, до package. Это не просто «так принято», это реально влияет на читаемость и на то, к чему относится аннотация.
Файл с «публичными значениями»: @JvmField
Создадим ExpenseConfig.kt:
package app.expenses
import kotlin.jvm.JvmField
@JvmField
val DEFAULT_CURRENCY: String = "RUB"
@JvmField
val API_VERSION: Int = 1
fun formatMoney(amount: Int): String {
return "$amount $DEFAULT_CURRENCY"
}
В Kotlin это будет удобно и как «конфиг», и как простой способ держать общие значения в одном месте.
Если когда-нибудь Java‑код захочет узнать валюту или версию API, ему не придётся вызывать getDEFAULT_CURRENCY() (и потом объяснять тимлиду, почему константа выглядит как геттер). Он сможет обращаться к полю.
Проверяем, что приложение по‑прежнему работает
Обновим main (упрощённый фрагмент — мы показываем идею, а не собираем весь проект на 200 строк):
package app
import app.expenses.API_VERSION
import app.expenses.formatMoney
import app.expenses.makeExpense
fun main() {
val expense = makeExpense("еда", 350, "обед")
println("API v$API_VERSION") // API v1
println("Сумма: ${formatMoney(expense.second)}") // Сумма: 350 RUB
}
Обратите внимание: для Kotlin‑кода вообще не важно, что там стоит @JvmField и @file:JvmName. Kotlin‑код читается как обычно. Но при этом JVM‑представление становится аккуратнее для внешнего потребителя.
5. Типичные ошибки
В этой теме ошибки особенно коварные: код может выглядеть «логично» в Kotlin, но быть неудобным (или даже неожиданным) для Java‑мира. Поэтому лучше сразу научиться узнавать типовые грабли по звуку.
Ошибка №1: поставить @file:JvmName не туда — например, после package.
File‑аннотации относятся ко всему файлу, поэтому они должны стоять до package. Если поставить их ниже, компилятор не «подумает, что вы имели в виду», а честно скажет, что так нельзя. Держите простое правило: file‑аннотация — вверху файла, затем пустая строка, затем package. Такой порядок прямо отражён в общих соглашениях оформления Kotlin‑кода.
Ошибка №2: ожидать, что Java увидит top‑level функцию «просто по имени», без класса.
На JVM функции не висят в воздухе: они будут методами в некотором классе. Если вы не управляете именем класса файла, Java почти наверняка увидит суффикс Kt. Это нормально, но если вы строите API — лучше сразу дать читаемое имя через @file:JvmName, чтобы не жить с техническими деталями в публичном интерфейсе.
Ошибка №3: попытаться повесить @JvmField на вычисляемое свойство.
Если у свойства нет backing field, то «экспортировать поле» невозможно физически: нет места хранения, есть только логика вычисления. Это частая ловушка, потому что в Kotlin val banner get() = ... выглядит как «переменная», но на JVM это чистый метод. В таком случае либо оставляйте обычный геттер (и Java будет вызывать getBanner()), либо делайте хранимое значение.
Ошибка №4: применять @JvmField «на всякий случай», не понимая, зачем.
@JvmField — не украшение и не «ускоритель Kotlin». Это инструмент interop‑дизайна. Если ваш модуль не будет использоваться из Java, то чаще всего вам не нужно превращать свойства в поля: геттеры/сеттеры — нормальная и безопасная форма. Аннотация нужна, когда вы точно хотите дать Java простой прямой доступ.
Ошибка №5: смешать в одном файле «всё подряд», а потом пытаться красиво назвать его для Java.
Если вы хотите экспортировать API‑поверхность, полезно держать отдельный файл (или несколько) под такие функции/значения. Тогда @file:JvmName("ExpenseApi") становится осмысленным: это реально «точка входа». А если в файле вперемешку main, парсинг, отчёты и случайная утилита trimAndNormalize(), то даже идеальное JVM‑имя не спасёт — API будет выглядеть как чердак, где «вроде всё полезное, но ходить страшно».
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ