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

386 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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<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`. Подробнее: [Функции-расширения](../Функциональный%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<String> { "как 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<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-оператора:
```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<String>()
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>` | `T?` | nullable-тип нативен |
| `new String[]{...}` | `arrayOf(...)` | короче |
| `StringBuilder` | интерполяция или `buildString` | читаемее |
```kotlin
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
```kotlin
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>` инвариантным. Подробнее: [Вариантность](../Дженерики/Вариантность.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<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-оператор меняет смысл:**
> ```kotlin
> 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?`. Подробнее: [Проекции типов](../Дженерики/Проекции%20типов.md)
>
> **Вызов Java-метода с `Object`-параметром принимает всё.** `method(Object o)` в Kotlin имеет параметр `Any!`, поэтому туда пройдёт и `null`, и любое значение - компилятор не поможет. Для таких API особенно важны аннотации.
>
> **SAM-конверсия не работает для абстрактных классов.** `Thread(Runnable { })` - да, но `object : Thread() { }` для переопределения `run()` - только через наследование. SAM применяется только к интерфейсам с единственным абстрактным методом.
Далее: [Kotlin из Java](Kotlin%20из%20Java.md)