Почти любой Kotlin-проект живёт рядом с Java: Android SDK, Spring, старые модули, библиотеки. На собесе спрашивают, как Kotlin видит Java, как Java видит Kotlin и где на этом стыке ломается код.
Java из Kotlin
Геттеры и сеттеры превращаются в свойства: user.getName() пишется как user.name, isActive() как user.isActive.
Платформенные типы. Если Java-метод не размечен аннотациями nullability, Kotlin видит тип String!: можно присвоить и в String, и в String?. Компилятор не проверяет, ответственность ваша. Аннотации @Nullable/@NonNull (JetBrains, AndroidX, JSpecify) превращают их в нормальные типы.
val name: String = javaUser.name // NPE здесь, если Java вернула null
val safe: String? = javaUser.name // безопасно
SAM-конверсия. Интерфейс Java с одним абстрактным методом принимает лямбду: executor.execute { work() }, view.setOnClickListener { }.
Ключевые слова Kotlin в Java-именах экранируются обратными кавычками: System.\in`, mockito.`when`(...)`.
Checked-исключения. В Kotlin их нет: можно вызвать Files.readString(path) без try и без throws.
Kotlin из Java
// Utils.kt
@file:JvmName("Utils")
package app
fun slug(s: String) = s.lowercase()
class User(val name: String) {
companion object {
@JvmStatic fun guest() = User("Гость")
const val MAX = 10
@JvmField val DEFAULT_NAME = "Гость"
}
}
| Kotlin | Как видит Java |
|---|---|
Функция верхнего уровня в Utils.kt | UtilsKt.slug(...), с @file:JvmName("Utils") будет Utils.slug(...) |
val name | getName(); var ещё и setName() |
companion object { fun guest() } | User.Companion.guest(); с @JvmStatic будет User.guest() |
@JvmField val x | публичное поле без геттера |
const val MAX | public static final константа |
object Config | Config.INSTANCE |
| Параметры по умолчанию | один метод со всеми параметрами; @JvmOverloads генерирует перегрузки |
fun f(): Unit | void f() |
Свойство isActive | isActive() и setActive() |
Checked-исключения и Java. Kotlin-функция, бросающая IOException, для Java не объявляет throws, и Java-код не сможет написать catch (IOException e): компилятор Java скажет, что оно не бросается. Решение: @Throws(IOException::class).
Null-проверки на границе. У публичных функций Kotlin с non-null параметрами компилятор вставляет проверку Intrinsics.checkNotNullParameter. Если Java передаст null, упадёт сразу на входе с понятным сообщением, а не глубже.
suspend-функции для Java выглядят как метод с дополнительным параметром Continuation, вызывать их из Java неудобно. Для Java-клиентов делают обёртки, возвращающие CompletableFuture (future { } из kotlinx-coroutines-jdk8, сейчас в core).
Дженерики и вариантность. Kotlin генерирует wildcards в сигнатурах для параметров: List<out T> в параметре станет List<? extends T>. Управлять этим можно аннотациями @JvmWildcard и @JvmSuppressWildcards (важно для Dagger/Hilt, когда multibinding не находит Set<Plugin>).
internal в байткоде становится public с искажённым именем (mangling), Java его увидит, но с неудобным именем.
Коллекции
Kotlin List и MutableList это один и тот же java.util.List. Java-метод, получив ваш «read-only» список, спокойно его изменит (если реализация изменяемая). Если важно, отдавайте копию.
Частые ошибки
- Присвоить платформенный тип в non-null переменную без проверки.
- Забыть
@JvmStatic/@JvmOverloads/@Throwsдля Java-клиентов. - Ждать, что
Listзащитит от изменений из Java. - Ловить в Java исключение, которое Kotlin-функция не объявила.
Что спросят на собесе
- Что такое платформенный тип и как от него защититься?
- Как вызвать функцию из
companion objectкак статическую? - Зачем
@JvmOverloads,@JvmField,@JvmName,@Throws? - Как Kotlin обходится без checked-исключений и что это значит для Java-клиентов?
- Как вызвать
suspend-функцию из Java?