Перейти к содержанию

Kotlin vs Java

Kotlin/JVM и Java используют общую JVM-экосистему и могут сосуществовать в одном проекте. Kotlin предлагает явную nullable-типизацию, компактный синтаксис и меньше шаблонного кода. На Android результат компиляции преобразуется в DEX и выполняется на ART, а не на настольной JVM.

Сравнение языков

Ключевые различия

  • В Kotlin есть nullable-типы, свойства, функции расширения, функции верхнего уровня, значения аргументов по умолчанию и именованные аргументы.
  • Классы и обычные члены Kotlin закрыты для наследования и переопределения по умолчанию.
  • Kotlin не требует обязательной обработки проверяемых исключений (checked exceptions).
  • В современной Java тоже есть records и sealed-типы. Это не исключительные возможности Kotlin, хотя их семантика различается.
  • Kotlin поддерживает suspend на уровне языка; построители корутин, диспетчеры и Flow в основном предоставляет kotlinx.coroutines. Приостанавливаемая функция не переходит автоматически на фоновый поток.

Коллекции Kotlin только для чтения не обязательно неизменяемы. Компактный код также не гарантирует ускорения: в критичных местах измеряйте выделение памяти и время выполнения.

Модификаторы видимости

Для обычных классов и членов Java отсутствие модификатора означает package-private; у членов интерфейсов правила другие. В Kotlin видимость по умолчанию обычно public.

Модификатор Значение в Kotlin Сравнение с Java
public Доступен везде, где доступно содержащее объявление. Видимость по умолчанию. Для классов и их членов обычно указывается явно.
private Доступ внутри объявляющего класса или файла для объявлений верхнего уровня. В Java нет функций верхнего уровня и прямого аналога видимости внутри файла.
protected Доступ внутри класса и подклассов; неприменим на верхнем уровне. Java дополнительно разрешает доступ из того же пакета.
internal Доступ внутри модуля компиляции Kotlin. Прямого аналога нет.
package-private Прямого модификатора нет. Доступ внутри пакета.

Модуль Kotlin - набор файлов, компилируемых вместе, например Gradle source set. Настроенные связи между дружественными модулями могут расширять доступ, в частности для тестов. На JVM объявления internal становятся публичными для Java. Имена внутренних членов могут изменяться компилятором (name mangling), но это не скрывает весь внутренний API и не служит границей безопасности.

Null-safety в Kotlin и Java

Kotlin различает String и String?. Для обращения к nullable-получателю нужны безопасный вызов (?.), проверка на null или явное утверждение вроде !!; оператор Elvis (?:) задаёт запасное значение.

Ссылочные типы Java сами по себе не кодируют допустимость null. Аннотации и инструменты анализа помогают, а Kotlin распознаёт поддерживаемые аннотации в Java API.

Ошибки из-за null всё ещё возможны при использовании !!, взаимодействии с Java и проблемах инициализации. Non-null тип уменьшает риск, но не проверяет произвольные внешние данные.

Платформенные типы

Java-ссылка с неизвестной допустимостью null может иметь в Kotlin платформенный тип, отображаемый инструментами как T!. Эту запись нельзя использовать в исходном Kotlin-коде.

Такое значение разрешено использовать и как nullable, и как non-null, но второй вариант может упасть, если Java вернёт null. На границе API принимайте значение в nullable-переменную и обрабатывайте отсутствие либо явно проверяйте контракт. Аннотации могут устранить эту неопределённость.

Проверяемые исключения

Kotlin не требует перехватывать проверяемое исключение Java или объявлять его в сигнатуре. Исключение всё равно может возникнуть, поэтому стратегия обработки зависит от контракта API.

Используйте @Throws, когда Java-коду нужно видеть проверяемое исключение в сгенерированной сигнатуре метода. Аннотация не обрабатывает исключение и не меняет его поведение во время выполнения.

JVM и взаимодействие с Java

Int: примитив или объект на JVM

В Kotlin есть единый тип Int, у которого можно вызывать методы. На JVM используется примитив int, где это возможно, а nullable-значения и обобщённые коллекции обычно требуют объектного представления Integer (boxing).

val count: Int = 10
val optionalCount: Int? = 10
val counts: List<Int> = listOf(1, 2, 3)
val packedCounts: IntArray = intArrayOf(1, 2, 3)

IntArray соответствует int[], а Array<Int> содержит объектные элементы. Оптимизации среды выполнения могут устранить часть выделений памяти, но boxing важен в критичном коде. Не используйте ссылочное равенство (===) для сравнения числовых значений.

Свойства Kotlin в Java

Публичный val обычно предоставляет getter, а публичный var - getter и setter, если видимость setter не ограничена. Поле хранения существует только при необходимости: у вычисляемого свойства его может не быть.

Например, val name: String предоставляет getName(), а var age: Int - getAge() и setAge(int). Для var isOpen: Boolean имена аксессоров - isOpen() и setOpen(boolean). Правило префикса is применяется и к другим типам, не только Boolean.

val означает отсутствие setter, а не неизменяемость возвращаемого объекта или постоянство результата пользовательского getter. Открытие поля через @JvmField или const val - отдельное решение при проектировании API.

Статические члены

В Kotlin нет ключевого слова static. Для самостоятельных функций и констант подходят объявления верхнего уровня, а object полезен, когда нужен единственный экземпляр объекта. companion object связывает объект с классом и позволяет обращаться к нему через имя класса в Kotlin.

Эти конструкции по-разному представлены на JVM. Сам по себе вызов через имя класса в Kotlin не делает метод статическим для Java.

Companion object из Java

К безымянному companion object из Java обычно обращаются через ClassName.Companion. Для именованного используется его объявленное имя.

class Parser {
    companion object {
        @JvmStatic
        fun parse(text: String): Int = text.toInt()
    }
}

Благодаря @JvmStatic Java может вызвать Parser.parse("42"). Для функции companion object также остаётся доступным метод экземпляра Parser.Companion.parse("42").

Функции верхнего уровня из Java

Функция из Utils.kt обычно становится статическим методом вида UtilsKt.someFunction(). Свойства верхнего уровня обычно предоставляют статические аксессоры, а не публичные поля.

@file:JvmName("BetterName") меняет имя фасадного класса. Чтобы объединить несколько файлов одного пакета под одним фасадом, укажите одинаковый @file:JvmName и @file:JvmMultifileClass в каждом файле. Функции расширения верхнего уровня тоже становятся статическими методами с получателем в качестве аргумента; виртуальные члены в класс получателя они не добавляют.

@JvmStatic, @JvmField, @JvmOverloads, @Throws

Аннотация Назначение для Java-кода
@JvmStatic Представляет функцию или аксессор свойства object/companion как статический метод.
@JvmField Открывает поле хранения подходящего свойства без аксессоров.
@JvmOverloads Генерирует перегрузки, последовательно убирая параметры со значениями по умолчанию с конца списка; не создаёт все комбинации аргументов.
@Throws Добавляет объявленные исключения в сигнатуру JVM-метода.

Без сгенерированных или написанных вручную перегрузок Java-код обычно передаёт все аргументы функции; синтаксиса именованных аргументов Kotlin в Java нет. Добавляйте аннотации там, где они улучшают Java API, а не ко всем объявлениям подряд.

open / final по умолчанию

Обычные классы Kotlin и конкретные члены по умолчанию final. Абстрактные члены неявно открыты, а override разрешает дальнейшее переопределение, если не указан final override. Чтобы подклассы могли переопределять открытый член, содержащий его класс тоже должен допускать наследование.

Обычный метод экземпляра Java обычно можно переопределить, если он не final и не private; статические методы скрываются, а не переопределяются. Java records являются final, поэтому утверждение «в Java всё открыто» тоже неточно.

Sealed-классы из Java

В Kotlin прямые наследники sealed-класса или интерфейса должны быть именованными и находиться в том же пакете и модуле. Для Multiplatform действуют дополнительные правила source set. Открытый прямой наследник может разрешить дальнейшее косвенное наследование.

Kotlin проверяет полноту вариантов в when. При JVM target 17 и выше компилятор записывает сведения о разрешённых подклассах, используемые sealed-иерархиями Java. Более низкие target не дают такого же ограничения на уровне JVM, особенно для sealed-интерфейсов.

Java тоже поддерживает исчерпывающую обработку sealed-типов при использовании подходящих возможностей языка. На Android проверяйте инструменты сборки, целевую версию байткода и поддержку desugaring: версия JDK для сборки сама по себе не определяет доступные возможности языка и среды выполнения.

Data classes и Java POJOs / records

Kotlin data class формирует equals(), hashCode(), toString(), copy() и componentN() по свойствам первичного конструктора с учётом правил генерации и существующих реализаций. Свойства тела класса не входят в это автоматически сформированное представление значения. Data-классы не могут быть open, abstract, sealed или inner.

Для Java POJO методы сравнения и представления обычно пишут вручную или генерируют. Java record имеет final-поля компонентов и аксессоры вида name(), но автоматически не получает аналоги Kotlin copy() и функций для деструктуризации. Свойства конструктора data-класса Kotlin могут быть val или var.

Ни одна модель не гарантирует глубокую неизменяемость. Kotlin copy() создаёт поверхностную копию: вложенные изменяемые объекты остаются общими. Data-класс Kotlin автоматически не становится JVM record.

Связанные темы

Источники