Kotlin
A concise multiplatform language developed by JetBrains
Публикации и ответы на комментарии в блогах JetBrains не выходят на русском языке с 2022 года.
Приносим извинения за неудобства.
Kotlin 1.4.30: новые возможности языка
Согласно новому графику релизов Kotlin 1.5 выйдет только через несколько месяцев, но попробовать новые возможности языка можно уже сейчас — в версии 1.4.30:
- Стабилизация inline-классов значений
- Экспериментальная поддержка JVM-записей
- Экспериментальная поддержка sealed-интерфейсов и другие улучшения sealed-классов
Чтобы попробовать новые функции, нужно указать версию языка 1.5.
Нам важно ваше мнение, поэтому мы будем рады получить от вас фидбек о новом синтаксисе.
Стабилизация inline-классов значений
В альфа-режиме inline-классы были представлены еще в Kotlin 1.3, а в версии 1.4.30 они перешагнули на стадию беты.
В Kotlin 1.5 inline-классы станут стабильными и будут относиться к более широкому понятию — классам значений, о которых мы рассказываем ниже.
Для начала вспомним, как работают inline-классы. Если вы с ними уже знакомы, смело переходите к следующему разделу.
Напоминаем, что inline-класс ликвидирует обертку вокруг значения:
Он может служить оберткой как для примитивного типа, так и для любого ссылочного типа, например String
.
Компилятор заменяет экземпляры inline-класса (в нашем примере экземпляр Color
) соответствующим типом (Int
) в байт-коде, если это возможно:
При этом компилятор генерирует функцию changeBackground
с искаженным именем, принимающую Int
в качестве параметра, и передает константу 255
напрямую, без создания обертки в месте вызова:
Имя искажено, чтобы обеспечить перегрузку функций, принимающих экземпляры различных inline-классов, и предотвратить случайные вызовы из Java-кода, которые могут нарушить внутренние ограничения inline-класса. Читайте далее, как сделать, чтобы это работало из Java.
Обертка не всегда удаляется в байт-коде. Это происходит, только если есть возможность, и очень похоже на примитивные типы. Когда вы определяете переменную типа Color
или передаете ее в функцию напрямую, она заменяется соответствующим значением:
В этом примере переменная color
во время компиляции имеет тип Color
, а в байт-коде заменена на Int
.
Однако если вы сохраните ее в коллекции или передадите в обобщенную функцию, она будет упакована в обычный объект типа Color
:
Упаковка и распаковка выполняются компилятором автоматически. Вам не нужно ничего с этим делать, но полезно знать, как это работает.
Изменение JVM-имен для вызовов из Java
Начиная с версии 1.4.30, вы можете изменить JVM-имя функции, принимающей inline-класс в качестве параметра, чтобы ее можно было вызывать из Java. По умолчанию такие имена иcкажаются, чтобы предотвратить случайное использование Java и конфликтующие перегрузки (например, changeBackground-euwHqFQ
в примере выше).
Аннотация @JvmName
изменяет имя функции в байт-коде и позволяет вызывать ее из Java и напрямую передать значение:
Как и любую функцию с аннотацией @JvmName
, из Kotlin вы вызываете ее по имени Kotlin. Использование в Kotlin типобезопасно, поскольку в качестве аргумента можно передать только значение типа Timeout
, а единицы измерения очевидны.
Из Java можно напрямую передать значение long
. Это уже не будет типобезопасно и поэтому не работает по умолчанию. Если вы видите в коде greetAfterTimeout(2)
, не сразу понятно, что имеется в виду: 2 секунды, 2 миллисекунды или 2 года.
Добавляя аннотацию, вы подчеркиваете, что собираетесь вызывать эту функцию из Java. Смысловое имя помогает избежать путаницы: добавление суффикса «Millis» делает единицы понятными для пользователей Java.
Блоки init
Еще одно улучшение inline-классов в 1.4.30 заключается в том, что теперь можно задавать логику инициализации в блоке init
:
Раньше так было делать нельзя.
Подробнее об inline-классах читайте в соответствующем KEEP, в документации и в комментариях к этой задаче.
Inline-классы значений
В Kotlin 1.5 inline-классы станут стабильными и будут относиться к более широкому понятию — классам значений.
До сих пор inline-классы были отдельной языковой конструкцией, а теперь представляют собой специальную JVM-оптимизацию класса значений с одним параметром. Классы значений — более широкий концепт, и в дальнейшем они будут поддерживать различные оптимизации. Сейчас они поддерживают inline-классы, а впоследствии будут поддерживать примитивные классы Valhalla, когда проект Valhalla будет завершен (подробности далее).
Единственное, что меняется сейчас, — это синтаксис. Поскольку inline-класс является оптимизированным классом значений, его следует объявлять по-новому:
Вы определяете класс значений с одним параметром конструктора и добавляете аннотацию @JvmInline
. Мы ожидаем, что с выходом Kotlin 1.5 все будут использовать новый синтаксис. Старый синтаксис inline class
будет работать еще некоторое время, но будет считаться устаревшим. В версии 1.5 появится предупреждение с предложением миграции. А впоследствии старый синтаксис будет считаться ошибочным.
Классы значений
Класс value
представляет собой неизменяемую сущность с данными. На данный момент такой класс может содержать только одно свойство для поддержки использования «старых» inline-классов.
В будущих версиях Kotlin можно будет определять классы значений с несколькими свойствами. Все значения должны быть неизменяемыми val
:
Классы значений не имеют идентичности: они полностью определяются хранящимися данными, и проверки === для них не разрешены. Проверка на равенство == автоматически сравнивает содержащиеся данные.
Такое отсутствие идентичности дает много возможностей для дальнейших оптимизаций: проекта Valhalla позволит реализовывать классы значений как примитивные JVM-классы.
Ограничение неизменности и, следовательно, возможность оптимизации отличает классы значений от data-классов.
Оптимизации из проекта Valhalla
Проект Valhalla предлагает новый концепт для Java и JVM — примитивные классы.
Основная цель примитивных классов — добавить к производительности примитивов объектно-ориентированные преимущества обычных JVM-классов. Примитивные классы — это носители данных, экземпляры которых могут храниться в переменных и в стеке вычислений. Работать с ними можно напрямую, без заголовков и указателей. В этом отношении они похожи на примитивные значения, такие как int
, long
и т. д. (в Kotlin вы не работаете с примитивными типами напрямую, но компилятор генерирует их под капотом).
Важным преимуществом примитивных классов является то, что они позволяют плоскую и плотную компоновку объектов в памяти. Сейчас Array<Point>
представляет собой массив ссылок. Проект Valhalla подразумевает, что при определении Point
как примитивного класса (в терминологии Java) или как класса значений с соответствующей оптимизацией (в терминологии Kotlin) JVM может оптимизировать его и хранить массив Point
в «плоской» компоновке, как массив из множества x
и y
, а не как массив ссылок.
Мы с нетерпением ждем предстоящих изменений в JVM и хотим, чтобы Kotlin извлек из них пользу. В то же время мы не хотели бы заставлять вас использовать только новые версии JVM для работы с value classes
, поэтому собираемся поддерживать и более ранние версий JVM. При компиляции кода в JVM с поддержкой Valhalla последние JVM-оптимизации будут работать для классов значений.
Изменяющие методы
О функциональности классов значений можно сказать гораздо больше. Поскольку классы значений представляют собой «неизменяемые» данные, для них возможны изменяющие методы, подобные тем, что есть в Swift: функция-член или сеттер свойства возвращает новый экземпляр, а не обновляет существующий. Главное преимущество — вы используете их со знакомым синтаксисом. Все это еще предстоит прототипировать в языке.
Где читать подробнее
Аннотация @JvmInline
предназначена для JVM. На других бэкендах классы значений могут быть реализованы иначе, например как структуры Swift в Kotlin/Native.
Читайте подробнее о классах значений в этом документе или смотрите отрывок из выступления Романа Елизарова.
Поддержка JVM-записей
Еще одно предстоящее улучшение экосистемы JVM — это Java records (записи). Они аналогичны data-классам в Kotlin и в основном используются как простые хранилища данных.
Записи Java не придерживаются соглашения JavaBeans, и в них используются методы «x()» и «y()» вместо привычных «getX()» и «getY()».
Совместимость с Java всегда была для нас в приоритете. Kotlin-код «понимает» новый синтаксис и рассматривает записи как классы со свойствами Kotlin. Работает так же, как и для обычных Java-классов, соответствующих JavaBeans:
В основном из-за совместимости нельзя аннотировать класс data
с помощью @JvmRecord
, чтобы сгенерировать новые методы JVM-записи:
Аннотация @JvmRecord
заставляет компилятор генерировать методы x()
и y()
вместо стандартных getX()
и getY()
. Предполагаем, что эта аннотация нужна вам только для сохранения API класса при его преобразовании из Java в Kotlin. Во всех остальных случаях можно без проблем использовать привычные data-классы Kotlin.
Аннотация доступна, только если вы компилируете Kotlin-код под JVM версии 15+. Подробнее об этой функции читайте в соответствующем KEEP, в документации и в комментариях к этой задаче.
Sealed-интерфейсы и улучшения sealed-классов
Когда класс изолирован (объявлен как sealed), иерархия ограничивается определенными подклассами — это позволяет проверить, что ветви when
охватывают все случаи. В Kotlin 1.4 иерархия sealed-классов имеет два ограничения. Во-первых, вышестоящий класс не может быть sealed-интерфейсом — он должен быть классом. Во-вторых, все подклассы должны находиться в одном файле.
В Kotlin 1.5 этих ограничений нет: изолированным можно сделать и интерфейс. Подклассы (как sealed-классов, так и sealed-интерфейсов) должны относиться к той же единице компиляции и к тому же пакету, что и родительский класс, но теперь они могут находиться в разных файлах.
Sealed-классы, а теперь и интерфейсы, полезны при создании иерархий абстрактных типов данных (ADT).
Еще один важный вариант использования sealed-интерфейсов — это ограничение наследования и реализации за пределами библиотеки. Интерфейс, определенный как sealed, может быть реализован только в той же единице компиляции и в том же пакете, что делает невозможным реализацию вне библиотеки.
Например, подразумевается, что интерфейс Job
из пакета kotlinx.coroutines
может быть реализован только внутри библиотеки kotlinx.coroutines
. Если сделать интерфейс изолированным, это будет выражено явно:
Как пользователю библиотеки вам больше нельзя определять собственный подкласс Job
. Это всегда «подразумевалось», но с sealed-интерфейсами может быть формально запрещено компилятором.
Использование JVM-поддержки в будущем
Предварительная поддержка sealed-классов была представлена в Java 15 и JVM. В дальнейшем мы собираемся использовать стандартную JVM-поддержку sealed-классов при компиляции Kotlin-кода под JVM последней версии (скорее всего, это будет JVM 17 или более поздняя версия — когда эта функция станет стабильной).
В Java вы явно перечисляете все подклассы данного изолированного класса или интерфейса:
Эта информация сохраняется в файле класса с помощью нового атрибута PermittedSubclasses
. JVM распознает изолированные классы во время выполнения и не дает расширять их неразрешенными подклассами.
В будущем, когда вы будете компилировать Kotlin-код, используя новейшую версию JVM, будет работать обновленная поддержка sealed-классов. Компилятор сгенерирует список разрешенных подклассов в байт-коде, чтобы обеспечить поддержку JVM и дополнительные проверки во время выполнения.
А в Kotlin список подклассов указывать не нужно. Компилятор сгенерирует список на основе объявленных подклассов в том же пакете.
Есть шанс, что возможность явно указывать подклассы для родительского класса будет добавлена позже в качестве дополнительной спецификации. Пока что мы не видим в этом необходимости, но будем рады услышать от вас, нужна ли вам такая функция.
Обратите внимание, что более старые версии JVM в теории позволяют определить Java-подкласс для изолированного интерфейса Kotlin, но лучше этого не делать. Поскольку поддержка разрешенных подклассов в JVM еще не доступна, ограничение применяется только компилятором Kotlin. Мы добавим предупреждения в IDE, чтобы этого не происходило случайно. В будущем новый механизм будет работать для новейших версий JVM, чтобы гарантировать отсутствие «неразрешенных» подклассов из Java.
Подробнее об изолированных интерфейсах и ослабленных ограничениях для изолированных классов читайте в соответствующем разделе KEEP, в документации и в комментариях к этой задаче.
Как все это попробовать
Чтобы попробовать новые возможности, используйте Kotlin 1.4.30 и укажите версию языка 1.5:
Чтобы попробовать JVM-записи, нужно также использовать jvmTarget 15 и включить JVM-превью: добавьте параметры компилятора -language-version 1.5
и -Xjvm-enable-preview
.
Примечания к предрелизной версии
Обратите внимание, что поддержка новых функций является экспериментальной, а поддержка версии 1.5 считается предварительной. Выбрав версию 1.5 в компиляторе Kotlin 1.4.30, вы будете как будто использовать превью-версию 1.5-M0. Гарантии обратной совместимости не распространяются на предварительные версии. Последующие релизы могут вносить изменения в функциональность и API. После выхода версии Kotlin 1.5-RC все бинарные файлы, созданные предрелизными версиями, будут запрещены в компиляторе: вам потребуется перекомпилировать все, что было скомпилировано в версиях 1.5-Mx.
Что думаете?
Пробуйте новые функции и делитесь своими впечатлениями! Подробнее о нововведениях читайте в соответствующих разделах KEEP и присоединяйтесь к обсуждениям в YouTrack. А если у вас что-то не работает, обязательно сообщите нам. Мы будем ждать отзывов о том, как новые возможности языка реализуются в ваших проектах.
Что еще почитать:
- Документация: что нового.
- Inline-классы — KEEP; обсуждение: KT-42434.
- Проектный документ о классах значений; обсуждение в KEEP.
- JVM-записи — KEEP; обсуждение: KT-42430.
- Sealed-интерфейсы и sealed-классы — KEEP; обсуждение: KT-42433.
Ваша команда Kotlin
The Drive to Develop