Перейти к основному содержимому

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).
SAMSingle Abstract Method — один метод в интерфейсе; Kotlin-лямбда подходит.
@JvmStaticМетод object виден в Java как статический.
@JvmOverloadsDefault-аргументы Kotlin → несколько перегрузок в Java.

Правило большого пальца

ПишетеДумайте о потребителе
Kotlin, вызывают из Java@JvmStatic, @JvmOverloads, @JvmName при необходимости
Java, вызывают из KotlinPlatform 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-конструктор Kotlin data class.
  • getId() и getName() показывают автогенерацию Java-friendly аксессоров.
KotlinЧто видит Java
object без @JvmStaticKotlinUtils.INSTANCE.twice()
@JvmStatic fun twiceKotlinUtils.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 явно, если код меняет коллекцию.


Постепенная миграция модуля

  1. Подключить Kotlin в Gradle/Maven (Первая программа на Kotlin).
  2. Новые классы — .kt в том же пакете, что Java.
  3. Заменить POJO на data class, сервисы — с конструкторной DI.
  4. Удалить .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(...)функции, вызываемые из Javachecked 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 и пустых значений прямо на границе, вместо разрозненных !! в бизнес-коде.


Пошаговая миграция без остановки разработки

  1. Оставить рабочий Java-код как baseline.
  2. Новые классы писать на Kotlin рядом со старыми.
  3. Переносить по вертикальным срезам: контроллер + сервис + тесты.
  4. После каждого среза прогонять unit и интеграционные тесты.
  5. Добавлять 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 в "безопасном" KotlinJava вернул null без @Nullable
KotlinUtils.INSTANCE.twiceНет @JvmStatic
Дубли методов в JavaКонфликт @JvmOverloads с ручными перегрузками
UnsupportedOperationExceptionИзменили read-only Kotlin List, обёрнутый над Java

Что попробовать

  1. Extension-функции Kotlin для "обёртки" над Java API.
  2. Spring Boot на Kotlin в проекте с Java-модулями.
  3. Один интерфейс на Kotlin, реализация в Java — и наоборот; unit-тест на границе.

Дальше

Spring Boot Kotlin · Room · Сравнение Java и Kotlin · о разделе