Kotlin guide done. (20.10.26)

This commit is contained in:
2026-10-20 12:54:25 +03:00
parent da3ad9dd1a
commit 6853ada348
81 changed files with 30112 additions and 0 deletions
@@ -0,0 +1,385 @@
---
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)