Files
obsidian/Kotlin/Взаимодействие с Java/Java из Kotlin.md
T

23 KiB
Raw Blame History

title, description, tags
title description tags
Java из Kotlin Платформенные типы, синтетические свойства, SAM-конверсия, массивы, статические члены и escape-хатчи
kotlin
java-interop

Вызов Java из Kotlin почти не требует усилий: Java-классы видны как обычные Kotlin-классы. Трение возникает в четырёх местах - null-безопасность, свойства, массивы и ключевые слова.

import java.io.File
import java.util.ArrayList

fun main() {
    val list = ArrayList<String>()
    list.add("первый")
    list.add("второй")
    println(list.size)                      // 2
    println(list.joinToString())             // первый, второй

    val tmp = File.createTempFile("demo", ".txt")
    tmp.writeText("содержимое")
    println(tmp.readText())                   // содержимое
    tmp.delete()
}

Обратите внимание: joinToString и writeText - это Kotlin-расширения, работающие на Java-типах. Stdlib добавляет сотни таких функций к java.util и java.io. Подробнее: Функции-расширения

Платформенные типы

Главная особенность: тип из Java записывается как String! - компилятор не знает, может ли там быть null.

fun main() {
    // System.getProperty возвращает String! — может быть null
    val asNonNull: String = System.getProperty("java.version")       // компилируется
    val asNullable: String? = System.getProperty("нет.такого")        // тоже компилируется

    println(asNonNull.isNotEmpty())        // true
    println(asNullable?.length)             // null
}

Компилятор разрешает оба варианта, поэтому ответственность на вас. Если Java вернёт null там, где объявлен ненулевой тип, проверка сработает в точке присваивания:

fun main() {
    val error = runCatching {
        val value: String = System.getProperty("отсутствует")
        value.length
    }.exceptionOrNull()
    println(error?.javaClass?.simpleName)      // NullPointerException
}

Сообщение будет вида «Parameter specified as non-null is null» или «null cannot be cast to non-null type» - проверка вставляется компилятором. Подробнее: Платформенные типы

Аннотации в Java решают проблему. Если Java-код аннотирован, платформенных типов не возникает:

// Java
@Nullable public String findName() { return null; }      // в Kotlin: String?
@NotNull public String getId() { return "1"; }            // в Kotlin: String
public String plain() { return null; }                     // в Kotlin: String!

Поддерживаются org.jetbrains.annotations, JSR-305 (javax.annotation), androidx.annotation, Lombok, Eclipse и Checker Framework. Это единственный способ получить настоящую null-безопасность на границе с Java, поэтому в смешанных проектах аннотировать Java-код стоит.

Синтетические свойства

Java-геттеры и сеттеры видны как свойства Kotlin:

import java.util.Calendar

fun main() {
    val cal = Calendar.getInstance()

    cal.timeInMillis = 0L                  // вместо setTimeInMillis(0)
    println(cal.timeInMillis)               // 0

    val sb = StringBuilder("текст")
    println(sb.length)                       // 5 — вместо length()
}

Правила распознавания:

Java-метод Свойство в Kotlin
getValue() value (только чтение)
getValue() + setValue(T) value (чтение и запись)
isReady() isReady (имя сохраняется)
setOnly(T) без геттера свойства нет, только метод
getValue(int index) свойства нет - есть параметр
fun main() {
    val thread = Thread.currentThread()
    println(thread.name.isNotEmpty())        // true  — getName()
    println(thread.isDaemon)                  // false — isDaemon() сохраняет префикс
    println(thread.priority)                   // 5     — getPriority()
}

Синтетические свойства доступны только для Java-методов. Kotlin-функция с именем getFoo() свойством не становится.

SAM-конверсия

Java-интерфейс с одним абстрактным методом принимает лямбду:

import java.util.concurrent.Executors

fun main() {
    val executor = Executors.newSingleThreadExecutor()

    executor.submit { println("как Runnable") }                  // лямбда -> Runnable
    val future = executor.submit<String> { "как Callable" }       // лямбда -> Callable
    println(future.get())

    val items = mutableListOf("гамма", "альфа", "бета")
    items.sortWith { a, b -> a.compareTo(b) }                     // Comparator
    println(items)
    executor.shutdown()
    // как Runnable
    // как Callable
    // [альфа, бета, гамма]
}

Когда перегрузок несколько и тип неоднозначен, интерфейс указывают явно:

import java.util.concurrent.Executors

fun main() {
    val executor = Executors.newSingleThreadExecutor()
    val task = Runnable { println("явная SAM-конструкция") }
    executor.submit(task)
    executor.shutdown()
    Thread.sleep(50)
    // явная SAM-конструкция
}

Важная тонкость: SAM-конверсия работает для Java-интерфейсов автоматически. Для Kotlin-интерфейса нужно объявить его как fun interface:

fun interface Validator {
    fun validate(input: String): Boolean
}

fun check(v: Validator, s: String) = v.validate(s)

fun main() {
    println(check({ it.isNotEmpty() }, "текст"))        // true
}

Подробнее: Лямбды и функциональные типы

Массивы

Java-массивы отображаются на типизированные классы Kotlin, причём примитивные - на специальные:

fun main() {
    val ints: IntArray = intArrayOf(1, 2, 3)                // int[]
    val strings: Array<String> = arrayOf("a", "b")            // String[]
    val boxed: Array<Int> = arrayOf(1, 2)                      // Integer[]

    println(ints::class.simpleName)            // IntArray
    println(strings::class.simpleName)          // Array
    println(ints.sum())                          // 6
    println(strings.joinToString())               // a, b
    println(boxed.size)                            // 2
}

IntArray компилируется в int[] без упаковки, а Array<Int> - в Integer[]. Это важно для производительности и для совместимости: Java-метод, принимающий int[], не примет Array<Int>.

Передача массива в varargs требует spread-оператора:

fun main() {
    val parts = arrayOf("a", "b", "c")
    println(String.format("%s-%s-%s", *parts))        // a-b-c

    val numbers = intArrayOf(3, 1, 2)
    println(listOf(*numbers.toTypedArray()).sorted())    // [1, 2, 3]
}

* распаковывает массив в отдельные аргументы - без него массив передался бы как один элемент.

Статические методы класса java.lang.String через Kotlin-овский String недоступны - String в Kotlin это kotlin.String, у него нет join:

fun main() {
    val parts = listOf("x", "y")
    println(java.lang.String.join("/", parts))        // x/y
    println(parts.joinToString("/"))                   // x/y — идиоматично
}

Статические члены

fun main() {
    println(Integer.MAX_VALUE)                  // 2147483647
    println(Integer.parseInt("42"))              // 42
    println(Math.max(3, 7))                       // 7

    val list = java.util.Collections.emptyList<String>()
    println(list.size)                             // 0
}

Статические методы и поля вызываются через имя класса, как в Java. Для часто используемых есть Kotlin-аналоги: "42".toInt() вместо Integer.parseInt, maxOf(3, 7) вместо Math.max, emptyList() вместо Collections.emptyList().

Java-ключевые слова как идентификаторы

Если Java-метод называется как ключевое слово Kotlin, его имя берётся в обратные кавычки:

class JavaLike {
    fun `is`(value: Int) = value > 0            // в Kotlin `is` — ключевое слово
    fun `in`(value: Int) = value
    fun `object`() = "объект"
}

fun main() {
    val j = JavaLike()
    println(j.`is`(5))           // true
    println(j.`in`(1))            // 1
    println(j.`object`())          // объект
}

Чаще всего это встречается с методами is, in, object, when, val, var, fun, typealias. Backticks работают и для имён с пробелами - этим пользуются в названиях тестов. Подробнее: kotlin.test и JUnit 5

Checked exceptions исчезают

import java.io.File

fun main() {
    // Java-метод объявляет throws IOException, но Kotlin этого не требует
    val tmp = File.createTempFile("demo", ".txt")
    tmp.writeText("данные")
    println(tmp.readText())
    tmp.delete()
    // данные
}

В Kotlin нет проверяемых исключений: try/catch вокруг createTempFile не обязателен. Это упрощает код, но требует дисциплины - компилятор больше не напоминает об обработке ошибок. Подробнее: Система исключений

Наследование от Java-класса

import java.io.IOException

class CustomException(message: String, cause: Throwable? = null) :
    IOException(message, cause)

abstract class BaseTask : Runnable {
    abstract fun execute()
    override fun run() {
        try {
            execute()
        } catch (e: Exception) {
            println("ошибка: ${e.message}")
        }
    }
}

class FailingTask : BaseTask() {
    override fun execute() = throw CustomException("задача упала")
}

fun main() {
    FailingTask().run()        // ошибка: задача упала
}

Наследование работает в обе стороны без ограничений: Kotlin-класс может расширять Java-класс, реализовывать Java-интерфейс и быть предком Java-класса.

Java-типы, которые заменяют на Kotlin-аналоги

Java Предпочтительно в Kotlin Почему
java.util.ArrayList mutableListOf() короче, тот же класс внутри
Integer.parseInt(s) s.toInt() расширение, читается слева направо
String.valueOf(x) x.toString() то же
Math.max(a, b) maxOf(a, b) работает с любым Comparable
Collections.sort(list) list.sort() расширение
Optional<T> T? nullable-тип нативен
new String[]{...} arrayOf(...) короче
StringBuilder интерполяция или buildString читаемее
fun main() {
    val fromJava = java.util.ArrayList<String>().apply { add("a") }
    val idiomatic = mutableListOf("a")
    println("тот же класс: ${fromJava::class == idiomatic::class}")      // тот же класс: true

    println(buildString {
        append("собрано ")
        append("через buildString")
    })
    // собрано через buildString
}

mutableListOf() возвращает именно java.util.ArrayList - разница только в синтаксисе.

Optional

import java.util.Optional

fun main() {
    val present: Optional<String> = Optional.of("значение")
    val empty: Optional<String> = Optional.empty()

    println(present.orElse("по умолчанию"))              // значение
    println(empty.orElse("по умолчанию"))                 // по умолчанию

    // перевод в идиоматичный Kotlin
    val asNullable: String? = present.orElse(null)
    println(asNullable?.length)                             // 8
    println(empty.getOrNull()?.length)                       // null
}

fun <T> Optional<T>.getOrNull(): T? = orElse(null)

Optional в Kotlin-коде избыточен: его роль выполняет T?, который не создаёт обёртку и поддержан синтаксисом (?., ?:, let). При получении Optional из Java-API его сразу переводят в nullable.

[!info] Отличия от Java

Java Kotlin
getName() name (синтетическое свойство)
isReady() isReady
new Runnable() { ... } { ... } (SAM)
int[] IntArray
Integer[] Array<Int>
method(array) для varargs method(*array)
throws IOException обязателен не требуется
Optional<T> T?

Платформенные типы - компромисс, которого в Java нет. В чистом Kotlin компилятор гарантирует отсутствие NPE. На границе с Java гарантия исчезает: тип String! означает «компилятор не знает». Альтернативой было бы считать все Java-типы nullable - тогда Java-код требовал бы ?. и !! на каждом шагу и стал бы неудобен. Авторы Kotlin выбрали практичность: ответственность за null на границе лежит на программисте.

Синтетические свойства работают только для Java. Это важная асимметрия: Java-метод getName() в Kotlin доступен и как getName(), и как .name, а Kotlin-функция getName() - только как функция. Причина в том, что в Kotlin для этого есть настоящие свойства, и превращать функции в свойства означало бы двусмысленность.

SAM-конверсия автоматическая только для Java-интерфейсов. В Java любой интерфейс с одним методом принимает лямбду. В Kotlin - нет: нужен fun interface, иначе требуется object : Interface { }. Причина - в Kotlin есть функциональные типы ((String) -> Boolean), которые покрывают большинство случаев, и SAM-интерфейсы нужны реже.

Массивы различаются сильнее, чем кажется. В Java int[] и Integer[] - разные типы, но оба «массивы». В Kotlin это IntArray и Array<Int>, причём Array<Int> ковариантен по-другому: Array<String> не является Array<Any>, тогда как в Java String[] является Object[] - что и породило ArrayStoreException. Kotlin эту дыру закрыл, сделав Array<T> инвариантным. Подробнее: Вариантность

Отсутствие проверяемых исключений - сознательное решение. Опыт Java показал, что throws чаще приводит к catch (Exception e) { } и обёрткам в RuntimeException, чем к осмысленной обработке, а в сочетании с лямбдами и дженериками создаёт неразрешимые проблемы (функциональный интерфейс не может объявить произвольный throws). Цена - потеря документирующей роли и необходимость @Throws при вызове из Java.

Сравнить: Массивы в Java, Система исключений в Java

[!warning] Подводные камни Платформенный тип даёт NPE в неожиданном месте:

fun main() {
    val props = System.getProperties()
    val error = runCatching {
        val missing: String = props.getProperty("нет.такого")
        missing.uppercase()
    }.exceptionOrNull()
    println("NPE при присваивании: ${error is NullPointerException}")
    // NPE при присваивании: true
}

Исключение возникает не там, где значение используется, а там, где оно присваивается ненулевому типу. Стектрейс указывает на строку присваивания, а не на Java-метод.

!! на платформенном типе бессмысленен. javaMethod()!! не добавляет безопасности - проверка и так вставляется. Правильно - ?: с осмысленным значением по умолчанию или явный тип String?.

Java-аннотации nullability обязательны для настоящей безопасности. Без них компилятор Kotlin не знает ничего, и весь Java-API остаётся зоной риска. Это основная причина добавлять @Nullable/@NotNull в унаследованный Java-код при миграции.

Неизменяемая Kotlin-коллекция, переданная в Java, может быть изменена:

fun main() {
    val readOnly = listOf("a")
    val error = runCatching { (readOnly as MutableList<String>).add("b") }
        .exceptionOrNull()
    println(error?.javaClass?.simpleName)      // UnsupportedOperationException
}

Java-код увидит java.util.List с методом add() и может его вызвать. Приведение компилируется, падение - в рантайме. Передавайте копию через toMutableList(), если Java-код изменяет коллекцию.

Array<Int> и IntArray не взаимозаменяемы. Java-метод с параметром int[] требует IntArray; arrayOf(1, 2) даёт Array<Int> (то есть Integer[]) и не подойдёт. Конвертация - toIntArray(), и она копирует данные.

Забытый spread-оператор меняет смысл:

fun main() {
    val parts = arrayOf("a", "b")
    println(listOf(parts).size)        // 1 — массив как один элемент
    println(listOf(*parts).size)        // 2 — распакованный
}

Компилятор не всегда предупреждает - listOf(parts) корректен, просто создаёт List<Array<String>>.

getClass() вызывается как javaClass. Метод Object.getClass() в Kotlin недоступен напрямую: он конфликтует с ::class. Используйте javaClass для java.lang.Class и ::class для KClass.

Статические вложенные Java-классы требуют полного пути. Map.Entry в Kotlin - это Map.Entry, но java.util.AbstractMap.SimpleEntry нужно импортировать явно; обращение через внешний класс (AbstractMap.SimpleEntry) работает только после импорта внешнего.

Lombok-геттеры видны, но не всегда. Lombok генерирует методы на этапе аннотационной обработки Java, и Kotlin-компилятор, работающий первым, их не видит. Нужен плагин kotlin("plugin.lombok") и lombok.config с lombok.addLombokGeneratedAnnotation = true.

Java-дженерики с wildcards переводятся в проекции. List<? extends Number> становится List<out Number>, List<? super Integer> - List<in Integer>. Raw-тип List без параметра становится List<*>, и элементы из него имеют тип Any?. Подробнее: Проекции типов

Вызов Java-метода с Object-параметром принимает всё. method(Object o) в Kotlin имеет параметр Any!, поэтому туда пройдёт и null, и любое значение - компилятор не поможет. Для таких API особенно важны аннотации.

SAM-конверсия не работает для абстрактных классов. Thread(Runnable { }) - да, но object : Thread() { } для переопределения run() - только через наследование. SAM применяется только к интерфейсам с единственным абстрактным методом.

Далее: Kotlin из Java