Kotlin и Java — совместимость на практике
Kotlin и Java — совместимость на практике
Kotlin и Java компилируются в один байт-код JVM. В одном Gradle/Maven-проекте файлы .kt и .java лежат рядом, классы вызывают друг друга напрямую. Сложности появляются на границе API — null, статические методы, коллекции, default-аргументы.
Обзор: экосистема Kotlin. Java-ООП: Объектно-ориентированное программирование в Java. Spring на Kotlin: Spring Boot на Kotlin — первая программа.
Словарь терминов
| Термин | Простыми словами |
|---|---|
| JVM | Виртуальная машина; исполняет .class независимо от исходного языка. |
Platform type T! | Тип из Java без null-аннотаций — Kotlin не знает, может ли быть null. |
@Nullable / @NotNull | Подсказки для компилятора Kotlin (JetBrains или javax). |
| SAM | Single Abstract Method — один метод в интерфейсе; Kotlin-лямбда подходит. |
@JvmStatic | Метод object виден в Java как статический. |
@JvmOverloads | Default-аргументы Kotlin → несколько перегрузок в Java. |
Правило большого пальца
| Пишете | Думайте о потребителе |
|---|---|
| Kotlin, вызывают из Java | @JvmStatic, @JvmOverloads, @JvmName при необходимости |
| Java, вызывают из Kotlin | Platform types, явные проверки ?. и !! только где уверены |
| Постепенная миграция | Новые файлы .kt, старые .java в том же пакете |
Вызов Java из Kotlin
Java:
package com.example;
import org.jetbrains.annotations.Nullable;
public class JavaGreeter {
public String greet(@Nullable String name) {
return "Hello, " + (name != null ? name : "World");
}
}
Разбор:
@Nullableна параметре сообщает Kotlin, что входное значение может бытьnull.- Тернарный оператор в Java подставляет
World, если имя не передано. - Контракт метода остаётся простым и предсказуемым для межъязыкового вызова.
- Этот пример показывает корректную Java-аннотацию для безопасного interop.
Kotlin:
fun main() {
val g = JavaGreeter()
val s: String? = g.greet(null)
println(s)
}
Разбор:
- Kotlin создаёт Java-класс и вызывает метод напрямую, без адаптеров.
- Тип
String?в переменнойsотражает nullable-контракт входного API. - Передача
nullпроверяет, что fallback-логика Java действительно отрабатывает. - Такой вызов удобен как smoke-проверка совместимости на этапе миграции.
Вызов Kotlin из Java
package com.example
object KotlinUtils {
@JvmStatic
fun twice(x: Int) = x * 2
@JvmOverloads
fun join(parts: List<String>, separator: String = ", ") =
parts.joinToString(separator)
}
data class User(val id: Long, val name: String)
Java:
int n = KotlinUtils.twice(21);
String line = KotlinUtils.join(List.of("a", "b"));
User u = new User(1L, "Ann");
long id = u.getId();
String name = u.getName();
Разбор:
KotlinUtils.twice(21)вызывается как static-метод благодаря@JvmStatic.join(List.of("a", "b"))использует перегрузку, сгенерированную@JvmOverloads.new User(...)работает как обычный Java-конструктор Kotlindata class.getId()иgetName()показывают автогенерацию Java-friendly аксессоров.
| Kotlin | Что видит Java |
|---|---|
object без @JvmStatic | KotlinUtils.INSTANCE.twice() |
@JvmStatic fun twice | KotlinUtils.twice() |
@JvmOverloads + default | Несколько методов join(...) |
data class User | Конструктор, getId(), getName(), copy, equals |
SAM и лямбды
Java:
public interface ClickListener {
void onClick();
}
Разбор:
- Интерфейс содержит единственный абстрактный метод, значит подходит под SAM.
- Такой контракт часто применяется для callback-событий в Java API.
- Простая сигнатура делает интерфейс удобным для Kotlin-лямбд.
- Это базовый паттерн межъязыковой интеграции событийного кода.
Kotlin:
val listener = ClickListener { println("click") }
Разбор:
- Kotlin автоматически преобразует лямбду в реализацию Java-интерфейса (SAM-конверсия).
- Тело лямбды становится реализацией метода
onClick(). - Код остаётся компактным и не требует анонимного класса.
- При этом объект полностью совместим с Java-ожиданиями по типу.
Лямбда Kotlin превращается в реализацию интерфейса с одним методом. Из Java 8+ лямбда передаётся в Kotlin-функцию ( ) -> Unit, если тип функциональный.
Коллекции и мутабельность
Java List из старого API может быть изменяемым ArrayList. Kotlin List — read-only view:
fun process(items: MutableList<String>) {
items.add("tail")
}
Разбор:
MutableList<String>явно показывает, что функция изменяет переданную коллекцию.items.add("tail")меняет исходный объект, а не возвращает новый список.- Такой контракт лучше документирует side effect на границе модулей.
- Явная мутабельность уменьшает риск неожиданных runtime-ошибок при interop.
На границе указывайте MutableList / java.util.List явно, если код меняет коллекцию.
Постепенная миграция модуля
- Подключить Kotlin в Gradle/Maven (Первая программа на Kotlin).
- Новые классы —
.ktв том же пакете, что Java. - Заменить POJO на
data class, сервисы — с конструкторной DI. - Удалить
.java, когда тесты зелёные.
Большой модуль одним коммитом без тестов ломает CI — мигрируйте по пакетам или по фичам.
Android — Java SDK + Kotlin UI
Android SDK на Java; UI на Kotlin (Jetpack Compose — первый экран) — обычная схема. Вызовы findViewById из Java Activity и Composable из Kotlin сосуществуют при постепенной миграции.
Interop-аннотации, которые чаще всего нужны
| Аннотация | Где применяют | Результат для Java-кода |
|---|---|---|
@JvmStatic | методы в object и companion object | вызов как статического метода |
@JvmOverloads | функции с default-аргументами | генерируются перегрузки |
@JvmField | поля в object/companion | доступ к полю без getter |
@file:JvmName("...") | top-level функции в файле | понятное имя utility-класса |
@Throws(...) | функции, вызываемые из Java | checked exceptions в сигнатуре |
Эти аннотации лучше добавлять точечно на публичных API-границах, а не по всему проекту.
Платформенные типы и безопасный шаблон
Если Java-метод без null-аннотаций, Kotlin видит T!. Практичный шаблон:
val raw = javaApi.loadUserName() // String!
val name = raw?.trim().orEmpty()
if (name.isBlank()) {
return "unknown"
}
return name
Разбор:
String!обозначает platform type, поэтому значение лучше нормализовать сразу на границе.raw?.trim().orEmpty()безопасно обрабатываетnullи лишние пробелы.- Проверка
isBlank()объединяет пустые и пробельные значения в единый случай. - Ранний
return "unknown"закрепляет стабильный контракт для дальнейшей логики.
Так вы фиксируете правило обработки null и пустых значений прямо на границе, вместо разрозненных !! в бизнес-коде.
Пошаговая миграция без остановки разработки
- Оставить рабочий Java-код как baseline.
- Новые классы писать на Kotlin рядом со старыми.
- Переносить по вертикальным срезам: контроллер + сервис + тесты.
- После каждого среза прогонять unit и интеграционные тесты.
- Добавлять interop-аннотации только там, где Java действительно вызывает Kotlin.
Такой подход держит CI зелёным и уменьшает риск больших регрессий.
Инструменты анализа на границе Kotlin и Java
- Включите строгий режим nullability для Java-аннотаций (
-Xjsr305=strict) поэтапно. - Используйте статический анализ из Java-раздела, чтобы отслеживать NPE и контракт null (PVS-Studio).
- Проверяйте публичные Kotlin API через небольшой Java smoke-тест, чтобы сразу видеть неудобные сигнатуры.
Дополнительный сниппет — top-level функция для Java
@file:JvmName("StringUtils")
package com.example
fun normalizeName(value: String?): String =
value?.trim().orEmpty().ifBlank { "unknown" }
Разбор:
@file:JvmName("StringUtils")задаёт имя utility-класса, который увидит Java-код.- Top-level функция компилируется в static-метод, удобный для вызова из Java.
- Сигнатура
String?явно показывает, что функция принимает nullable-значение. - Цепочка
trim().orEmpty().ifBlank { ... }централизует нормализацию входных строк. - Такой формат полезен для общих helper-функций на границе Kotlin и Java модулей.
String name = StringUtils.normalizeName(rawValue);
Разбор:
- Java вызывает Kotlin top-level функцию как обычный static-метод класса
StringUtils. - Такой вызов снижает порог миграции: Java-команда использует привычный синтаксис.
- Общая точка нормализации уменьшает дублирование null/blank-проверок в кодовой базе.
- Контракт остаётся единым для двух языков и упрощает поддержку API-границы.
Частые ошибки
| Симптом | Причина |
|---|---|
| NPE в "безопасном" Kotlin | Java вернул null без @Nullable |
KotlinUtils.INSTANCE.twice | Нет @JvmStatic |
| Дубли методов в Java | Конфликт @JvmOverloads с ручными перегрузками |
UnsupportedOperationException | Изменили read-only Kotlin List, обёрнутый над Java |
Что попробовать
- Extension-функции Kotlin для "обёртки" над Java API.
- Spring Boot на Kotlin в проекте с Java-модулями.
- Один интерфейс на Kotlin, реализация в Java — и наоборот; unit-тест на границе.
Дальше
Spring Boot Kotlin · Room · Сравнение Java и Kotlin · о разделе