--- title: Java из Kotlin description: Платформенные типы, синтетические свойства, SAM-конверсия, массивы, статические члены и escape-хатчи tags: - kotlin - java-interop --- Вызов Java из Kotlin почти не требует усилий: Java-классы видны как обычные Kotlin-классы. Трение возникает в четырёх местах - null-безопасность, свойства, массивы и ключевые слова. ```kotlin import java.io.File import java.util.ArrayList fun main() { val list = ArrayList() 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`. Подробнее: [Функции-расширения](../Функциональный%20Kotlin/Функции-расширения.md) ## Платформенные типы Главная особенность: тип из Java записывается как `String!` - компилятор не знает, может ли там быть `null`. ```kotlin 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` там, где объявлен ненулевой тип, проверка сработает в точке присваивания: ```kotlin 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» - проверка вставляется компилятором. Подробнее: [Платформенные типы](../Синтаксис%20и%20типы/Null-безопасность/Платформенные%20типы.md) **Аннотации в Java решают проблему.** Если 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: ```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)` | свойства нет - есть параметр | ```kotlin 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-интерфейс с одним абстрактным методом принимает лямбду: ```kotlin import java.util.concurrent.Executors fun main() { val executor = Executors.newSingleThreadExecutor() executor.submit { println("как Runnable") } // лямбда -> Runnable val future = executor.submit { "как Callable" } // лямбда -> Callable println(future.get()) val items = mutableListOf("гамма", "альфа", "бета") items.sortWith { a, b -> a.compareTo(b) } // Comparator println(items) executor.shutdown() // как Runnable // как Callable // [альфа, бета, гамма] } ``` Когда перегрузок несколько и тип неоднозначен, интерфейс указывают явно: ```kotlin 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`: ```kotlin fun interface Validator { fun validate(input: String): Boolean } fun check(v: Validator, s: String) = v.validate(s) fun main() { println(check({ it.isNotEmpty() }, "текст")) // true } ``` Подробнее: [Лямбды и функциональные типы](../Синтаксис%20и%20типы/Работа%20с%20данными/Лямбды%20и%20функциональные%20типы.md) ## Массивы Java-массивы отображаются на типизированные классы Kotlin, причём примитивные - на специальные: ```kotlin fun main() { val ints: IntArray = intArrayOf(1, 2, 3) // int[] val strings: Array = arrayOf("a", "b") // String[] val boxed: Array = 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` - в `Integer[]`. Это важно для производительности и для совместимости: Java-метод, принимающий `int[]`, не примет `Array`. Передача массива в varargs требует spread-оператора: ```kotlin 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`: ```kotlin fun main() { val parts = listOf("x", "y") println(java.lang.String.join("/", parts)) // x/y println(parts.joinToString("/")) // x/y — идиоматично } ``` ## Статические члены ```kotlin fun main() { println(Integer.MAX_VALUE) // 2147483647 println(Integer.parseInt("42")) // 42 println(Math.max(3, 7)) // 7 val list = java.util.Collections.emptyList() println(list.size) // 0 } ``` Статические методы и поля вызываются через имя класса, как в Java. Для часто используемых есть Kotlin-аналоги: `"42".toInt()` вместо `Integer.parseInt`, `maxOf(3, 7)` вместо `Math.max`, `emptyList()` вместо `Collections.emptyList()`. ## Java-ключевые слова как идентификаторы Если Java-метод называется как ключевое слово Kotlin, его имя берётся в обратные кавычки: ```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](../Тестирование/kotlin.test%20и%20JUnit%205.md) ## Checked exceptions исчезают ```kotlin 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` не обязателен. Это упрощает код, но требует дисциплины - компилятор больше не напоминает об обработке ошибок. Подробнее: [Система исключений](../Исключения/Система%20исключений.md) ## Наследование от Java-класса ```kotlin 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?` | nullable-тип нативен | | `new String[]{...}` | `arrayOf(...)` | короче | | `StringBuilder` | интерполяция или `buildString` | читаемее | ```kotlin fun main() { val fromJava = java.util.ArrayList().apply { add("a") } val idiomatic = mutableListOf("a") println("тот же класс: ${fromJava::class == idiomatic::class}") // тот же класс: true println(buildString { append("собрано ") append("через buildString") }) // собрано через buildString } ``` `mutableListOf()` возвращает именно `java.util.ArrayList` - разница только в синтаксисе. ## Optional ```kotlin import java.util.Optional fun main() { val present: Optional = Optional.of("значение") val empty: Optional = 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 Optional.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` | > | `method(array)` для varargs | `method(*array)` | > | `throws IOException` обязателен | не требуется | > | `Optional` | `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`, причём `Array` ковариантен по-другому: `Array` не является `Array`, тогда как в Java `String[]` является `Object[]` - что и породило `ArrayStoreException`. Kotlin эту дыру закрыл, сделав `Array` инвариантным. Подробнее: [Вариантность](../Дженерики/Вариантность.md) > > **Отсутствие проверяемых исключений - сознательное решение.** Опыт Java показал, что `throws` чаще приводит к `catch (Exception e) { }` и обёрткам в `RuntimeException`, чем к осмысленной обработке, а в сочетании с лямбдами и дженериками создаёт неразрешимые проблемы (функциональный интерфейс не может объявить произвольный `throws`). Цена - потеря документирующей роли и необходимость `@Throws` при вызове из Java. > > Сравнить: [Массивы в Java](../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Массивы.md), [Система исключений в Java](../../Программирование%20на%20Java/Исключения/Система%20исключений.md) > [!warning] Подводные камни > **Платформенный тип даёт NPE в неожиданном месте:** > ```kotlin > 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, может быть изменена:** > ```kotlin > fun main() { > val readOnly = listOf("a") > val error = runCatching { (readOnly as MutableList).add("b") } > .exceptionOrNull() > println(error?.javaClass?.simpleName) // UnsupportedOperationException > } > ``` > Java-код увидит `java.util.List` с методом `add()` и может его вызвать. Приведение компилируется, падение - в рантайме. Передавайте копию через `toMutableList()`, если Java-код изменяет коллекцию. > > **`Array` и `IntArray` не взаимозаменяемы.** Java-метод с параметром `int[]` требует `IntArray`; `arrayOf(1, 2)` даёт `Array` (то есть `Integer[]`) и не подойдёт. Конвертация - `toIntArray()`, и она копирует данные. > > **Забытый spread-оператор меняет смысл:** > ```kotlin > fun main() { > val parts = arrayOf("a", "b") > println(listOf(parts).size) // 1 — массив как один элемент > println(listOf(*parts).size) // 2 — распакованный > } > ``` > Компилятор не всегда предупреждает - `listOf(parts)` корректен, просто создаёт `List>`. > > **`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` становится `List`, `List` - `List`. Raw-тип `List` без параметра становится `List<*>`, и элементы из него имеют тип `Any?`. Подробнее: [Проекции типов](../Дженерики/Проекции%20типов.md) > > **Вызов Java-метода с `Object`-параметром принимает всё.** `method(Object o)` в Kotlin имеет параметр `Any!`, поэтому туда пройдёт и `null`, и любое значение - компилятор не поможет. Для таких API особенно важны аннотации. > > **SAM-конверсия не работает для абстрактных классов.** `Thread(Runnable { })` - да, но `object : Thread() { }` для переопределения `run()` - только через наследование. SAM применяется только к интерфейсам с единственным абстрактным методом. Далее: [Kotlin из Java](Kotlin%20из%20Java.md)