386 lines
23 KiB
Markdown
386 lines
23 KiB
Markdown
---
|
||
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)
|