Интероп с Java
Место встречи двух языков — платформенные типы из неаннотированной Java, @JvmStatic, @JvmOverloads и @JvmName, @Throws, отображение объявлений Kotlin в байткод, вызов suspend-функции из Java и места, где стопроцентный интероп даёт течь.
12 вопросов
JuniorТеорияОчень частоУ функции Kotlin есть аргументы по умолчанию. Что видит Java и что делает @JvmOverloads?
У функции Kotlin есть аргументы по умолчанию. Что видит Java и что делает @JvmOverloads?
В Java нет аргументов по умолчанию, поэтому функция компилируется в один метод со всеми параметрами — Java обязана передать их все. Аннотация @JvmOverloads велит компилятору сгенерировать цепочку перегрузок, по одной на каждый отброшенный хвостовой параметр.
Типичные ошибки
- ✗Считать, что Java может опустить аргумент по умолчанию без
@JvmOverloads - ✗Думать, что
@JvmOverloadsчто-то меняет для вызовов из Kotlin - ✗Ждать перегрузку на каждую комбинацию параметров, а не на каждый хвостовой
Уточняющие вопросы
- →Почему
@JvmOverloadsгенерирует перегрузки только по хвостовым параметрам, а не по всем комбинациям? - →Что добавить конструктору с аргументами по умолчанию, чтобы Java могла звать короткую форму?
JuniorТеорияОчень частоЧто такое платформенный тип T! и почему Kotlin всё равно может бросить NPE на таком значении?
Что такое платформенный тип T! и почему Kotlin всё равно может бросить NPE на таком значении?
У значения из неаннотированной Java nullability неизвестна, поэтому компилятор даёт платформенный тип T! и разрешает использовать его как T или T?: он ничего не требует. При присваивании в non-null T вставленная проверка бросит NPE, если значение реально null.
Типичные ошибки
- ✗Верить, что null-безопасность Kotlin работает и для значений из неаннотированной Java
- ✗Читать
T!как синонимT?и ждать, что компилятор потребует проверку на null - ✗Винить в NPE Kotlin, а не отсутствующий
@Nullableна стороне Java
Уточняющие вопросы
- →Какие аннотации Java заставляют Kotlin отобразить возвращаемое значение в
String?, а не вString!? - →Где именно компилятор вставляет проверку на null для платформенного типа?
JuniorТеорияЧастоКак Java вызывает член companion object из Kotlin и что меняет @JvmStatic?
Как Java вызывает член companion object из Kotlin и что меняет @JvmStatic?
companion object — объект-синглтон, а не статическая секция, поэтому Java идёт через него: Foo.Companion.bar(). @JvmStatic на члене заставляет компилятор выпустить и статический метод у Foo, и Java пишет Foo.bar(); член companion остаётся вызываемым.
Типичные ошибки
- ✗Считать, что
companion objectкомпилируется вstatic-члены Java - ✗Звать
Foo.bar()из Java без@JvmStaticи получать ошибку компиляции - ✗Думать, что
@JvmStaticубирает форму вызоваFoo.Companion.bar()
Уточняющие вопросы
- →Что меняет
@JvmFieldна свойстве companion для вызывающего кода на Java? - →Где ещё можно поставить
@JvmStatic, кроме членаcompanion object?
JuniorТеорияЧастоКак свойство val или var из Kotlin выглядит для использующего его кода на Java?
Как свойство val или var из Kotlin выглядит для использующего его кода на Java?
Свойство компилируется в приватное поле-хранилище плюс аксессоры: val name даёт Java метод getName(), а var добавляет setName(). Java зовёт методы, а не поле. Свойство Boolean с именем isDone сохраняет имя: геттер isDone(), сеттер setDone().
Типичные ошибки
- ✗Ждать, что Java обратится к свойству Kotlin как к public-полю
- ✗Забывать, что у
Booleanпо имениisDoneгеттер остаётсяisDone(), а неgetIsDone() - ✗Думать, что
valвсё равно даёт Java сеттер
Уточняющие вопросы
- →Что меняет
@JvmFieldна свойстве Kotlin для вызывающего кода на Java? - →Как переименовать только геттер, который видит Java, не переименовывая свойство?
JuniorТеорияИногдаПочему вызов Java из Kotlin почти бесплатен, а вызов Kotlin из Java требует усилий?
Почему вызов Java из Kotlin почти бесплатен, а вызов Kotlin из Java требует усилий?
Kotlin читает объявления Java напрямую, поэтому это направление почти бесплатно; ловушка одна — платформенные типы с неизвестной nullability. Обратное упирается в то, чего в Java нет: аргументов по умолчанию, а члены companion object не статические.
Типичные ошибки
- ✗Считать, что null-безопасность Kotlin защищает значения из неаннотированной Java
- ✗Ждать, что Java вызовет функцию Kotlin с аргументами по умолчанию, опустив их
- ✗Думать, что член
companion objectкомпилируется в статический метод
Уточняющие вопросы
- →Какая аннотация делает член
companion objectвызываемым из Java как статический? - →Что видит Java, когда функция Kotlin объявляет значения аргументов по умолчанию?
MiddleТеорияИногдаЧто @JvmName меняет для вызывающего кода на Java и какой конфликт она классически чинит?
Что @JvmName меняет для вызывающего кода на Java и какой конфликт она классически чинит?
@JvmName меняет только имя, видимое из Java: функции, аксессора или файлового класса. Классическая задача — конфликт стирания: две функции, отличающиеся лишь типовым аргументом, дают одну JVM-сигнатуру, и одну надо переименовать. В Kotlin имя остаётся прежним.
Типичные ошибки
- ✗Думать, что
@JvmNameпереименовывает объявление и для вызовов из Kotlin - ✗Ждать, что две функции с одинаковой стёртой сигнатурой скомпилируются без переименования
- ✗Забывать, что файловый класс для Java называется
FooKt, пока его не переименует@file:JvmName
Уточняющие вопросы
- →Почему две функции Kotlin, отличающиеся лишь типовым аргументом, сталкиваются на JVM?
- →Где должна стоять
@file:JvmNameи что именно она переименовывает?
MiddleТеорияИногдаНеаннотированная библиотека Java даёт платформенные типы. Как вернуть настоящую nullability?
Неаннотированная библиотека Java даёт платформенные типы. Как вернуть настоящую nullability?
Проставьте в исходниках Java аннотации nullability, которые Kotlin понимает (JSpecify, JetBrains), — и T! схлопнется в настоящий T или T?. Если библиотека не ваша, оберните её тонкой границей на Kotlin с честными типами и одной проверкой вместо !! повсюду.
Типичные ошибки
- ✗Считать
!!на каждом вызове починкой, а не переносом падения в другое место - ✗Думать, что Kotlin игнорирует аннотации nullability на объявлениях Java
- ✗Ждать, что компилятор сам проверит платформенный тип на null
Уточняющие вопросы
- →Какие семейства аннотаций nullability компилятор Kotlin действительно понимает?
- →Что должна делать обёртка на Kotlin, когда значение из Java реально
null?
MiddleТеорияИногдаКак Java добирается до члена object Foo из Kotlin и как укоротить такой вызов?
Как Java добирается до члена object Foo из Kotlin и как укоротить такой вызов?
object компилируется в класс, хранящий единственный экземпляр в статическом поле INSTANCE, поэтому Java пишет Foo.INSTANCE.bar(). Пометьте член @JvmStatic — компилятор добавит статический форвардер, и Java пишет Foo.bar(); поле INSTANCE остаётся.
Типичные ошибки
- ✗Ждать, что
Foo.bar()заработает из Java дляobjectбез@JvmStatic - ✗Путать поле
INSTANCEуobjectс полемCompanionу класса - ✗Думать, что
@JvmStaticубирает форму вызова черезINSTANCE
Уточняющие вопросы
- →Когда на самом деле инициализируется синглтон, стоящий за
objectв Kotlin? - →Что
@JvmFieldна свойствеobjectоткрывает для Java?
MiddleТеорияИногдаКак suspend fun выглядит для Java и как открыть её вызывающему коду на Java?
Как suspend fun выглядит для Java и как открыть её вызывающему коду на Java?
suspend fun компилируется в метод с лишним хвостовым параметром Continuation, возвращающий Object — либо результат, либо маркер приостановки. Java не умеет его собрать, поэтому вызов заворачивают в CoroutineScope и отдают в Java CompletableFuture или колбэк.
Типичные ошибки
- ✗Считать, что Java вызовет
suspend fun, передавnullвместоContinuation - ✗Ждать, что
suspend funсама отдаст в JavaCompletableFuture - ✗Думать, что
@JvmStaticили@JvmNameделаютsuspend funпригодной для Java
Уточняющие вопросы
- →Что на самом деле возвращает
suspend funв момент, когда корутина приостанавливается? - →Какой билдер корутин отдаёт в Java
CompletableFutureдляsuspend fun?
MiddleТеорияИногдаПочему Java не может поймать checked-исключение, брошенное кодом на Kotlin, и как это чинится?
Почему Java не может поймать checked-исключение, брошенное кодом на Kotlin, и как это чинится?
В Kotlin нет checked-исключений, и секция throws не выпускается; для javac метод ничего не объявляет, и обёртка вызова в catch (IOException e) не компилируется. Поставьте @Throws(IOException::class) на объявление в Kotlin — компилятор впишет секцию в сигнатуру.
Типичные ошибки
- ✗Считать, что Kotlin выпускает секцию
throws, раз функция действительно может бросить - ✗Верить, что
@Throwsчто-то меняет для вызывающего кода на Kotlin - ✗Писать
try/catchв Java, не добавив@Throwsна стороне Kotlin
Уточняющие вопросы
- →Меняет ли
@Throwsто, как вызывающий код на Kotlin обязан обработать это исключение? - →Что
@Throwsвыпускает дляsuspend fun, если смотреть со стороны Java?
MiddleТеорияРедкоМожет ли Java вызвать inline fun <reified T> из Kotlin? Обоснуйте через то, как она компилируется.
Может ли Java вызвать inline fun <reified T> из Kotlin? Обоснуйте через то, как она компилируется.
Из Java она не вызывается вообще. reified существует только потому, что функция inline: компилятор подставляет реальный T в каждое место вызова из Kotlin, поэтому обычного метода с пригодным T не остаётся. Для Java делают не-inline перегрузку с Class<T>.
Типичные ошибки
- ✗Считать, что
reifiedдоживает до байткода как скрытый параметрClass, который передаст Java - ✗Верить, что
@JvmNameили@JvmStaticоткрываютreified-функцию для Java - ✗Забывать, что
reifiedработает только за счёт встраивания тела в места вызова из Kotlin
Уточняющие вопросы
- →Какую сигнатуру вы дадите не-inline перегрузке, которую должна звать Java?
- →Почему
reified-параметр типа обязан находиться уinline-функции?
SeniorТеорияРедкоKotlin заявляет стопроцентную совместимость с Java. Где это утверждение даёт течь?
Kotlin заявляет стопроцентную совместимость с Java. Где это утверждение даёт течь?
Вызов Java из Kotlin бесшовен, кроме платформенных типов, стирающих null-безопасность. Течёт обратный вызов: аргументам по умолчанию нужен @JvmOverloads, reified-функцию не вызвать, suspend fun получает Continuation, а internal публичен под искажённым именем.
Типичные ошибки
- ✗Читать полную совместимость как симметричную гарантию в обе стороны
- ✗Считать, что
internalпрячет объявление от Java - ✗Ждать, что Java опустит аргумент по умолчанию или вызовет
reified-функцию
Уточняющие вопросы
- →Как функция-расширение выглядит в сигнатуре, которую видит Java?
- →Что мешает Java исчерпывающе обработать иерархию
sealedиз Kotlin?