23 KiB
title, description, tags
| title | description | tags | ||
|---|---|---|---|---|
| Java из Kotlin | Платформенные типы, синтетические свойства, SAM-конверсия, массивы, статические члены и escape-хатчи |
|
Вызов 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()isReadynew Runnable() { ... }{ ... }(SAM)int[]IntArrayInteger[]Array<Int>method(array)для varargsmethod(*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>, тогда как в JavaString[]является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