Руководство по миграции на компилятор K2
По мере развития языка Kotlin и его экосистемы развивался и компилятор Kotlin. Первым шагом стало появление новых бэкендов JVM и JS IR (промежуточное представление), которые используют общую логику, упрощая генерацию кода для целевых платформ. Теперь следующий этап развития — новый фронтенд под названием K2.
С появлением компилятора K2 фронтенд Kotlin был полностью переписан и получил новую, более эффективную архитектуру. Основное изменение, которое привносит новый компилятор, — использование единой структуры данных, содержащей больше семантической информации. Этот фронтенд отвечает за семантический анализ, разрешение вызовов и вывод типов.
Новая архитектура и обогащенная структура данных позволяют компилятору K2 предоставить следующие преимущества:
Улучшенное разрешение вызовов и вывод типов. Компилятор работает более последовательно и лучше понимает ваш код.
Упрощение добавления синтаксического сахара для новых возможностей языка. В будущем вы сможете писать более лаконичный и понятный код при появлении новых возможностей.
Ускорение компиляции. Компиляция может выполняться значительно быстрее.
Повышение производительности IDE. IntelliJ IDEA и Android Studio используют компилятор K2 для анализа кода Kotlin, повышая стабильность и производительность. Дополнительную информацию см. в разделе Поддержка в IDE.
В этом руководстве:
Объясняются преимущества нового компилятора K2.
Рассматриваются изменения, с которыми вы можете столкнуться при миграции, и способы адаптации кода.
Описывается, как вернуться к предыдущей версии.
Повышение производительности
Чтобы оценить производительность компилятора K2, мы провели тесты производительности на двух проектах с открытым исходным кодом: Anki-Android и Exposed. Вот основные улучшения производительности, которые мы обнаружили:
Компилятор K2 обеспечивает ускорение компиляции до 94%. Например, в проекте Anki-Android время полной сборки сократилось с 57,7 секунды в Kotlin 1.9.23 до 29,7 секунды в Kotlin 2.0.0.
Фаза инициализации с компилятором K2 выполняется до 488% быстрее. Например, в проекте Anki-Android фаза инициализации инкрементальной сборки сократилась с 0,126 секунды в Kotlin 1.9.23 до всего 0,022 секунды в Kotlin 2.0.0.
Компилятор Kotlin K2 выполняет фазу анализа до 376% быстрее по сравнению с предыдущим компилятором. Например, в проекте Anki-Android время анализа при инкрементальной сборке сократилось с 0,581 секунды в Kotlin 1.9.23 до всего 0,122 секунды в Kotlin 2.0.0.
Подробнее об этих улучшениях и о том, как мы анализировали производительность компилятора K2, читайте в нашей публикации в блоге.
Улучшения возможностей языка
Компилятор Kotlin K2 улучшает возможности языка, связанные со смарт-кастами и Kotlin Multiplatform.
Смарт-касты
В определенных случаях компилятор Kotlin может автоматически привести объект к типу, избавляя вас от необходимости указывать это явно. Это называется смарт-кастом. Теперь компилятор Kotlin K2 выполняет смарт-касты в еще большем количестве сценариев.
В Kotlin 2.0.0 мы улучшили работу смарт-кастов в следующих областях:
Локальные переменные и последующие области видимости
Ранее, если в условии if переменная оценивалась как не null, для нее выполнялся смарт-каст. Информация об этой переменной затем распространялась дальше в пределах области видимости блока if.
Однако, если вы объявляли переменную до условия if, внутри условия if информация о переменной была недоступна, поэтому смарт-каст для нее выполнить было невозможно. Такое поведение наблюдалось также в выражениях when и циклах while.
Начиная с Kotlin 2.0.0, если вы объявляете переменную перед ее использованием в условии if, when или while, любая собранная компилятором информация о переменной будет доступна в соответствующем блоке для выполнения смарт-каста.
Это может быть полезно, например, когда вы хотите вынести логические условия в переменные. Тогда вы сможете дать переменной понятное имя, что повысит читаемость кода и позволит повторно использовать ее позже. Например:
class Cat {
fun purr() {
println("Purr purr")
}
}
fun petAnimal(animal: Any) {
val isCat = animal is Cat
if (isCat) {
// In Kotlin 2.0.0, the compiler can access
// information about isCat, so it knows that
// animal was smart-cast to the type Cat.
// Therefore, the purr() function can be called.
// In Kotlin 1.9.20, the compiler doesn't know
// about the smart cast, so calling the purr()
// function triggers an error.
animal.purr()
}
}
fun main(){
val kitty = Cat()
petAnimal(kitty)
// Purr purr
}
Проверки типов с оператором логического ИЛИ
В Kotlin 2.0.0, если объединить проверки типов объектов оператором or (||), смарт-каст выполняется к их ближайшему общему супертипу. До этого изменения смарт-каст всегда выполнялся к типу Any.
В этом случае вам все еще нужно было вручную проверить тип объекта, прежде чем получить доступ к его свойствам или вызвать его функции. Например:
interface Status {
fun signal() {}
}
interface Ok : Status
interface Postponed : Status
interface Declined : Status
fun signalCheck(signalStatus: Any) {
if (signalStatus is Postponed || signalStatus is Declined) {
// signalStatus is smart-cast to a common supertype Status
signalStatus.signal()
// Prior to Kotlin 2.0.0, signalStatus is smart cast
// to type Any, so calling the signal() function triggered an
// Unresolved reference error. The signal() function can only
// be called successfully after another type check:
// check(signalStatus is Status)
// signalStatus.signal()
}
}
Встроенные функции
В Kotlin 2.0.0 компилятор K2 обрабатывает встроенные функции иначе, что позволяет ему в сочетании с другими видами анализа компилятора определять, безопасно ли выполнять смарт-каст.
В частности, теперь встроенные функции считаются имеющими неявный контракт callsInPlace. Это означает, что все лямбда-функции, переданные встроенной функции, вызываются на месте. Поскольку лямбда-функции вызываются на месте, компилятор знает, что лямбда-функция не может вынести ссылки на переменные, содержащиеся в ее теле.
Компилятор использует эти сведения наряду с результатами других видов анализа, чтобы определить, безопасно ли выполнять смарт-каст для захваченных переменных. Например:
interface Processor {
fun process()
}
inline fun inlineAction(f: () -> Unit) = f()
fun nextProcessor(): Processor? = null
fun runProcessor(): Processor? {
var processor: Processor? = null
inlineAction {
// In Kotlin 2.0.0, the compiler knows that processor
// is a local variable and inlineAction() is an inline function, so
// references to processor can't be leaked. Therefore, it's safe
// to smart-cast processor.
// If processor isn't null, processor is smart-cast
if (processor != null) {
// The compiler knows that processor isn't null, so no safe call
// is needed
processor.process()
// In Kotlin 1.9.20, you have to perform a safe call:
// processor?.process()
}
processor = nextProcessor()
}
return processor
}
Свойства с функциональными типами
В предыдущих версиях Kotlin была ошибка, из-за которой для свойств класса с функциональным типом не выполнялся смарт-каст. В Kotlin 2.0.0 и компиляторе K2 это поведение исправлено. Например:
class Holder(val provider: (() -> Unit)?) {
fun process() {
// In Kotlin 2.0.0, if provider isn't null,
// it is smart-cast
if (provider != null) {
// The compiler knows that provider isn't null
provider()
// In 1.9.20, the compiler doesn't know that provider isn't
// null, so it triggers an error:
// Reference has a nullable type '(() -> Unit)?', use explicit '?.invoke()' to make a function-like call instead
}
}
}
Это изменение также распространяется на случаи, когда вы перегружаете оператор invoke. Например:
interface Provider {
operator fun invoke()
}
interface Processor : () -> String
class Holder(val provider: Provider?, val processor: Processor?) {
fun process() {
if (provider != null) {
provider()
// In 1.9.20, the compiler triggers an error:
// Reference has a nullable type 'Provider?', use explicit '?.invoke()' to make a function-like call instead
}
}
}
Обработка исключений
В Kotlin 2.0.0 мы улучшили обработку исключений, чтобы информацию о смарт-касте можно было передавать в блоки catch и finally. Это делает ваш код безопаснее, поскольку компилятор отслеживает, имеет ли объект nullable-тип. Например:
//sampleStart
fun testString() {
var stringInput: String? = null
// stringInput is smart-cast to String type
stringInput = ""
try {
// The compiler knows that stringInput isn't null
println(stringInput.length)
// 0
// The compiler rejects previous smart cast information for
// stringInput. Now stringInput has the String? type.
stringInput = null
// Trigger an exception
if (2 > 1) throw Exception()
stringInput = ""
} catch (exception: Exception) {
// In Kotlin 2.0.0, the compiler knows stringInput
// can be null, so stringInput stays nullable.
println(stringInput?.length)
// null
// In Kotlin 1.9.20, the compiler says that a safe call isn't
// needed, but this is incorrect.
}
}
//sampleEnd
fun main() {
testString()
}
Операторы инкремента и декремента
До Kotlin 2.0.0 компилятор не понимал, что тип объекта может измениться после применения оператора инкремента или декремента. Поскольку компилятор не мог точно отслеживать тип объекта, в вашем коде могли возникать ошибки неразрешенной ссылки. В Kotlin 2.0.0 эта проблема исправлена:
interface Rho {
operator fun inc(): Sigma = TODO()
}
interface Sigma : Rho {
fun sigma() = Unit
}
interface Tau {
fun tau() = Unit
}
fun main(input: Rho) {
var unknownObject: Rho = input
// Check if unknownObject inherits from the Tau interface
// Note, it's possible that unknownObject inherits from both
// Rho and Tau interfaces.
if (unknownObject is Tau) {
// Use the overloaded inc() operator from interface Rho.
// In Kotlin 2.0.0, the type of unknownObject is smart-cast to
// Sigma.
++unknownObject
// In Kotlin 2.0.0, the compiler knows unknownObject has type
// Sigma, so the sigma() function can be called successfully.
unknownObject.sigma()
// In Kotlin 1.9.20, the compiler doesn't perform a smart cast
// when inc() is called so the compiler still thinks that
// unknownObject has type Tau. Calling the sigma() function
// throws a compile-time error.
// In Kotlin 2.0.0, the compiler knows unknownObject has type
// Sigma, so calling the tau() function throws a compile-time
// error.
unknownObject.tau()
// Unresolved reference 'tau'
// In Kotlin 1.9.20, since the compiler mistakenly thinks that
// unknownObject has type Tau, the tau() function can be called,
// but it throws a ClassCastException.
}
}
Kotlin Multiplatform
В компиляторе K2 появились улучшения, связанные с Kotlin Multiplatform, в следующих областях:
Разделение общих и платформенных исходных файлов во время компиляции
Ранее устройство компилятора Kotlin не позволяло ему разделять общие и платформенные наборы исходных файлов во время компиляции. В результате общий код мог обращаться к платформенному, что приводило к различиям в поведении на разных платформах. Кроме того, некоторые настройки компилятора и зависимости из общего кода могли попадать в платформенный код.
В Kotlin 2.0.0 при реализации нового компилятора Kotlin K2 схема компиляции была переработана, чтобы обеспечить строгое разделение общих и платформенных наборов исходных файлов. Это изменение наиболее заметно при использовании expect- и actual-функций. Ранее вызов функции в общем коде мог разрешиться в функцию из платформенного кода. Например:
Общий код |
Платформенный код |
|---|---|
fun foo(x: Any) = println("common foo")
fun exampleFunction() {
foo(42)
}
|
// JVM
fun foo(x: Int) = println("platform foo")
// JavaScript
// There is no foo() function overload on the JavaScript platform
|
В этом примере поведение общего кода зависит от платформы, на которой он выполняется:
На платформе JVM вызов функции
foo()в общем коде приводит к вызову функцииfoo()из платформенного кода в качествеplatform foo.На платформе JavaScript вызов функции
foo()в общем коде приводит к вызову функцииfoo()из общего кода в качествеcommon foo, поскольку в платформенном коде такой функции нет.
В Kotlin 2.0.0 общий код не имеет доступа к платформенному, поэтому на обеих платформах функция foo() успешно разрешается в функцию foo() из общего кода: common foo.
Помимо повышения согласованности поведения на разных платформах, мы также приложили немало усилий, чтобы устранить случаи расхождения между IntelliJ IDEA или Android Studio и компилятором. Например, при использовании expect- и actual-классов происходило следующее:
Общий код |
Платформенный код |
|---|---|
expect class Identity {
fun confirmIdentity(): String
}
fun common() {
// Before 2.0.0, it triggers an IDE-only error
Identity().confirmIdentity()
// RESOLUTION_TO_CLASSIFIER : Expected class Identity has no default constructor.
}
|
actual class Identity {
actual fun confirmIdentity() = "expect class fun: jvm"
}
|
В этом примере у ожидаемого класса Identity нет конструктора по умолчанию, поэтому его нельзя успешно вызвать в общем коде. Ранее об ошибке сообщала только IDE, но код при этом успешно компилировался для JVM. Теперь же компилятор правильно сообщает об ошибке:
Expected class 'expect class Identity : Any' does not have default constructor
Когда поведение разрешения не меняется
Мы все еще переходим на новую схему компиляции, поэтому поведение разрешения остается прежним при вызове функций, находящихся не в одном наборе исходных файлов. Это различие особенно заметно при использовании в общем коде перегрузок из многоплатформенной библиотеки.
Предположим, у вас есть библиотека с двумя функциями whichFun() с разными сигнатурами:
// Example library
// MODULE: common
fun whichFun(x: Any) = println("common function")
// MODULE: JVM
fun whichFun(x: Int) = println("platform function")
Если вызвать функцию whichFun() в общем коде, будет выбрана функция из библиотеки, тип аргумента которой наиболее подходит:
// A project that uses the example library for the JVM target
// MODULE: common
fun main(){
whichFun(2)
// platform function
}
Для сравнения, если объявить перегрузки для whichFun() в одном наборе исходных файлов, будет выбрана функция из общего кода, поскольку у вашего кода нет доступа к платформенной версии:
// Example library isn't used
// MODULE: common
fun whichFun(x: Any) = println("common function")
fun main(){
whichFun(2)
// common function
}
// MODULE: JVM
fun whichFun(x: Int) = println("platform function")
Как и многоплатформенные библиотеки, модуль commonTest по-прежнему имеет доступ к платформенному коду, поскольку находится в отдельном наборе исходных файлов. Поэтому разрешение вызовов функций в модуле commonTest ведет себя так же, как и в старой схеме компиляции.
В будущем эти оставшиеся случаи будут приведены в большее соответствие с новой схемой компиляции.
Разные уровни видимости expect- и actual-объявлений
До Kotlin 2.0.0 при использовании expect- и actual-объявлений в проекте Kotlin Multiplatform они должны были иметь одинаковый уровень видимости. Теперь Kotlin 2.0.0 также поддерживает разные уровни видимости, но только если actual-объявление более доступно, чем expect-объявление. Например:
expect internal class Attribute // Visibility is internal
actual class Attribute // Visibility is public by default,
// which is more permissive
Аналогично, если в actual-объявлении используется псевдоним типа, видимость базового типа должна быть такой же или более высокой, чем у expect-объявления. Например:
expect internal class Attribute // Visibility is internal
internal actual typealias Attribute = Expanded
class Expanded // Visibility is public by default,
// which is more permissive
Как включить компилятор Kotlin K2
Начиная с Kotlin 2.0.0, компилятор Kotlin K2 включен по умолчанию.
Чтобы обновить версию Kotlin, укажите версию 2.0.0 или более позднюю в сценариях сборки Gradle и Maven.
Использование отчетов о сборке Kotlin с Gradle
Отчеты о сборке Kotlin содержат сведения о времени, затраченном на разные этапы компиляции задач компилятора Kotlin, а также о том, какой компилятор и версия Kotlin использовались и была ли компиляция инкрементальной. Эти отчеты полезны для оценки производительности сборки. Они дают больше информации о конвейере компиляции Kotlin, чем отчеты о сборках Gradle, поскольку содержат обзор производительности всех задач Gradle.
Как включить отчеты о сборке
Чтобы включить отчеты о сборке, укажите, куда сохранять их вывод, в файле gradle.properties:
kotlin.build.report.output=file
Для вывода доступны следующие значения и их комбинации:
Параметр |
Описание |
|---|---|
|
Сохраняет отчеты о сборке в удобочитаемом формате в локальный файл. По умолчанию это |
|
Сохраняет отчеты о сборке в формате объекта в указанный локальный файл. |
|
Сохраняет отчеты о сборке в разделе |
|
Отправляет отчеты о сборке с помощью HTTP(S). Метод POST отправляет метрики в формате JSON. Актуальную версию отправляемых данных можно посмотреть в репозитории Kotlin. Примеры конечных точек HTTP приведены в этой публикации в блоге |
|
Сохраняет отчеты о сборке в формате JSON в локальный файл. Укажите расположение отчетов о сборке в |
Дополнительную информацию о возможностях отчетов о сборке см. в разделе Отчеты о сборке.
Поддержка в IDE
IntelliJ IDEA и Android Studio полностью поддерживают компилятор K2 и используют его по умолчанию для улучшения анализа кода, автодополнения и подсветки. Ничего настраивать не нужно. Обновите IDE до последней версии, чтобы воспользоваться преимуществами.
Попробуйте компилятор Kotlin K2 в Kotlin Playground
Kotlin Playground поддерживает Kotlin 2.0.0 и более поздние версии. Попробуйте!
Как вернуться к предыдущему компилятору
Чтобы использовать предыдущий компилятор в Kotlin 2.0.0–2.3.21, выполните одно из следующих действий:
-
В файле
build.gradle.ktsустановите версию языка1.9.ИЛИ
Используйте следующий параметр компилятора:
-language-version 1.9.
Начиная с Kotlin 2.4.0, вернуться к предыдущему компилятору нельзя.
Изменения
С появлением нового фронтенда компилятор Kotlin претерпел несколько изменений. Сначала рассмотрим наиболее значимые изменения, влияющие на ваш код: объясним, что изменилось, и расскажем о рекомендуемых подходах в дальнейшем. Если вы хотите узнать больше, мы сгруппировали изменения по темам, чтобы вам было проще продолжить чтение.
В этом разделе рассматриваются следующие изменения:
Немедленная инициализация открытых свойств с полями для хранения значения
Устаревшие синтетические сеттеры для проецируемого получателя
Единый порядок разрешения свойств Kotlin и полей Java с одинаковыми именами
Улучшенная проверка на null для массивов примитивных типов Java
Более строгие правила для абстрактных членов в ожидаемых классах
Немедленная инициализация открытых свойств с полями для хранения значения
Что изменилось?
В Kotlin 2.0 все свойства open с полями для хранения значения должны инициализироваться сразу, иначе возникнет ошибка компиляции. Раньше немедленная инициализация требовалась только для свойств open var, но теперь это правило распространяется и на свойства open val с полями для хранения значения:
open class Base {
open val a: Int
open var b: Int
init {
// Error starting with Kotlin 2.0 that earlier compiled successfully
this.a = 1 //Error: open val must have initializer
// Always an error
this.b = 1 // Error: open var must have initializer
}
}
class Derived : Base() {
override val a: Int = 2
override var b = 2
}
Это изменение делает поведение компилятора более предсказуемым. Рассмотрим пример, в котором свойство open val переопределяется свойством var с пользовательским сеттером.
Использование пользовательского сеттера может привести к путанице при отложенной инициализации, поскольку непонятно, нужно ли инициализировать поле для хранения значения или вызвать сеттер. Раньше компилятор не мог гарантировать, что при вызове сеттера поле для хранения значения будет инициализировано.
Как теперь лучше поступать?
Мы рекомендуем всегда инициализировать открытые свойства с полями для хранения значения, поскольку считаем, что такой подход эффективнее и снижает вероятность ошибок.
Однако, если вы не хотите инициализировать свойство сразу, можно:
Сделать свойство
final.Использовать приватное свойство для хранения значения, которое допускает отложенную инициализацию.
Дополнительную информацию см. в соответствующей задаче в YouTrack.
Устаревший синтетический сеттер для проецируемого получателя
Что изменилось?
Если вы используете синтетический сеттер класса Java, чтобы присвоить значение типа, конфликтующего с проецируемым типом класса, возникает ошибка.
Предположим, что у вас есть класс Java с именем Container, содержащий методы getFoo() и setFoo():
public class Container<E> {
public E getFoo() {
return null;
}
public void setFoo(E foo) {}
}
Если в следующем коде Kotlin экземпляры класса Container имеют проецируемые типы, использование метода setFoo() всегда приводит к ошибке. Однако синтетическое свойство foo будет вызывать ошибку только начиная с Kotlin 2.0.0:
fun exampleFunction(starProjected: Container<*>, inProjected: Container<in Number>, sampleString: String) {
starProjected.setFoo(sampleString)
// Error since Kotlin 1.0
// Synthetic setter `foo` is resolved to the `setFoo()` method
starProjected.foo = sampleString
// Error since Kotlin 2.0.0
inProjected.setFoo(sampleString)
// Error since Kotlin 1.0
// Synthetic setter `foo` is resolved to the `setFoo()` method
inProjected.foo = sampleString
// Error since Kotlin 2.0.0
}
Как теперь лучше поступать?
Если из-за этого изменения в коде появились ошибки, возможно, стоит пересмотреть структуру объявлений типов. Может быть, вам не нужно использовать проекции типов или следует удалить из кода присваивания.
Дополнительную информацию см. в соответствующей задаче в YouTrack.
Запрет на использование недоступных обобщённых типов
Что изменилось?
В связи с новой архитектурой компилятора K2 мы изменили способ обработки недоступных обобщённых типов. Как правило, не следует использовать недоступные обобщённые типы в коде, поскольку это указывает на неправильную конфигурацию сборки проекта, из-за которой компилятор не может получить информацию, необходимую для компиляции. В Kotlin 2.0.0 нельзя объявить или вызвать функциональный литерал с недоступным обобщённым типом, а также использовать обобщённый тип с недоступными аргументами типа. Это ограничение помогает избежать ошибок компилятора в дальнейшем.
Например, предположим, что вы объявили обобщённый класс в одном модуле:
// Module one class Node<V>(val value: V)
Если у вас есть другой модуль (модуль два), настроенный как зависимый от модуля один, ваш код может обращаться к классу Node<V> и использовать его как тип в типах функций:
// Module two
fun execute(func: (Node<Int>) -> Unit) {}
// Function compiles successfully
Однако, если проект настроен неправильно и у вас есть третий модуль (модуль три), зависящий только от модуля два, компилятор Kotlin не сможет получить доступ к классу Node<V> в модуле один при компиляции третьего модуля. Теперь любые лямбды или анонимные функции в модуле три, использующие тип Node<V>, вызывают ошибки в Kotlin 2.0.0. Это помогает предотвратить ошибки компилятора, сбои и исключения во время выполнения, которых можно избежать:
// Module three
fun test() {
// Triggers an error in Kotlin 2.0.0, as the type of the implicit
// lambda parameter (it) resolves to Node, which is inaccessible
execute {}
// Triggers an error in Kotlin 2.0.0, as the type of the unused
// lambda parameter (_) resolves to Node, which is inaccessible
execute { _ -> }
// Triggers an error in Kotlin 2.0.0, as the type of the unused
// anonymous function parameter (_) resolves to Node, which is inaccessible
execute(fun (_) {})
}
Ошибки возникают не только при использовании в функциональных литералах параметров-значений с недоступными обобщёнными типами, но и в том случае, если тип содержит недоступный аргумент обобщённого типа.
Например, в модуле один объявлен тот же обобщённый класс. В модуле два объявлен другой обобщённый класс: Container<C>. Кроме того, в модуле два объявлены функции, в которых Container<C> использует обобщённый класс Node<V> в качестве аргумента типа:
Модуль один |
Модуль два |
|---|---|
// Module one class Node<V>(val value: V) |
// Module two
class Container<C>(vararg val content: C)
// Functions with generic class type that
// also have a generic class type argument
fun produce(): Container<Node<Int>> = Container(Node(42))
fun consume(arg: Container<Node<Int>>) {}
|
Если вызвать эти функции в модуле три, в Kotlin 2.0.0 возникнет ошибка, поскольку обобщённый класс Node<V> недоступен из модуля три:
// Module three
fun test() {
// Triggers an error in Kotlin 2.0.0, as generic class Node<V> is
// inaccessible
consume(produce())
}
В будущих выпусках мы продолжим отказываться от использования недоступных типов в целом. В Kotlin 2.0.0 мы уже начали это делать, добавив предупреждения для некоторых случаев с недоступными типами, в том числе необобщёнными.
Например, возьмём ту же конфигурацию модулей, что и в предыдущих примерах, но заменим обобщённый класс Node<V> необобщённым классом IntNode и объявим все функции в модуле два:
Модуль один |
Модуль два |
|---|---|
// Module one class IntNode(val value: Int) |
// Module two
// A function that contains a lambda
// parameter with `IntNode` type
fun execute(func: (IntNode) -> Unit) {}
class Container<C>(vararg val content: C)
// Functions with generic class type
// that has `IntNode` as a type argument
fun produce(): Container<IntNode> = Container(IntNode(42))
fun consume(arg: Container<IntNode>) {}
|
При вызове этих функций в модуле три появляются предупреждения:
// Module three
fun test() {
// Triggers warnings in Kotlin 2.0.0, as class IntNode is
// inaccessible.
execute {}
// Class 'IntNode' of the parameter 'it' is inaccessible.
execute { _ -> }
execute(fun (_) {})
// Class 'IntNode' of the parameter '_' is inaccessible.
// Will trigger a warning in future Kotlin releases, as IntNode is
// inaccessible.
consume(produce())
}
Как теперь лучше поступать?
Если вы столкнулись с новыми предупреждениями о недоступных обобщённых типах, скорее всего, проблема связана с конфигурацией системы сборки. Рекомендуем проверить скрипты сборки и конфигурацию.
В крайнем случае можно настроить прямую зависимость модуля три от модуля один. Другой вариант — изменить код так, чтобы типы были доступны в рамках одного модуля.
Дополнительную информацию см. в соответствующей задаче в YouTrack.
Единый порядок разрешения свойств Kotlin и полей Java с одинаковыми именами
Что изменилось?
До Kotlin 2.0.0 при работе с классами Java и Kotlin, наследующими друг от друга и содержащими свойства Kotlin и поля Java с одинаковыми именами, поведение при разрешении совпадающих имён было непоследовательным. Кроме того, поведение IntelliJ IDEA и компилятора различалось. Разрабатывая новый механизм разрешения для Kotlin 2.0.0, мы стремились свести влияние на пользователей к минимуму.
Например, предположим, что есть класс Java Base:
public class Base {
public String a = "a";
public String b = "b";
}
Предположим также, что есть класс Kotlin Derived, наследующий ранее упомянутый класс Base:
class Derived : Base() {
val a = "aa"
// Declares custom get() function
val b get() = "bb"
}
fun main() {
// Resolves Derived.a
println(a)
// aa
// Resolves Base.b
println(b)
// b
}
До Kotlin 2.0.0 выражение a разрешалось как свойство Kotlin в классе Kotlin Derived, а выражение b — как поле Java в классе Java Base.
В Kotlin 2.0.0 поведение разрешения в этом примере стало последовательным: свойство Kotlin имеет приоритет над одноимённым полем Java. Теперь выражение b разрешается как: Derived.b.
Общее правило: приоритет имеет подкласс. Это видно на предыдущем примере: разрешается свойство Kotlin a из класса Derived, поскольку Derived — подкласс класса Java Base.
Если наследование организовано наоборот и класс Java наследует класс Kotlin, приоритет имеет поле Java в подклассе, а не одноимённое свойство Kotlin.
Рассмотрим пример:
Kotlin |
Java |
|---|---|
open class Base {
val a = "aa"
}
|
public class Derived extends Base {
public String a = "a";
}
|
Теперь рассмотрим следующий код:
fun main() {
// Resolves Derived.a
println(a)
// a
}
Как теперь лучше поступать?
Если это изменение влияет на ваш код, подумайте, действительно ли вам нужны совпадающие имена. Если вы хотите, чтобы классы Java или Kotlin содержали поле или свойство с одинаковым именем и наследовали друг друга, учитывайте, что приоритет будет у поля или свойства в подклассе.
Дополнительную информацию см. в соответствующей задаче в YouTrack.
Улучшенная проверка на null для массивов примитивных типов Java
Что изменилось?
Начиная с Kotlin 2.0.0 компилятор корректно определяет допустимость null для массивов примитивных типов Java, импортированных в Kotlin. Теперь он сохраняет исходную информацию о допустимости null из аннотаций TYPE_USE, используемых с массивами примитивных типов Java, и выдаёт ошибки, если значения используются не в соответствии с аннотациями.
Обычно при вызове из Kotlin типов Java с аннотациями @Nullable и @NotNull им присваивается соответствующая информация о допустимости null:
interface DataService {
@NotNull ResultContainer<@Nullable String> fetchData();
}
val dataService: DataService = ... dataService.fetchData() // -> ResultContainer<String?>
Однако раньше при импорте массивов примитивных типов Java в Kotlin все аннотации TYPE_USE терялись, что приводило к платформенной допустимости null и потенциально небезопасному коду:
interface DataProvider {
int @Nullable [] fetchData();
}
val dataService: DataProvider = ... dataService.fetchData() // -> IntArray .. IntArray? // No error, even though `dataService.fetchData()` might be `null` according to annotations // This might result in a NullPointerException dataService.fetchData()[0]
Обратите внимание, что эта проблема никогда не затрагивала аннотации допустимости null непосредственно в объявлении, а только аннотации TYPE_USE.
Как теперь лучше поступать?
В Kotlin 2.0.0 проверка на null для массивов примитивных типов Java стала стандартной. Если вы используете такие массивы, проверьте код на наличие новых предупреждений и ошибок:
Любой код, который использует массив примитивного типа Java
@Nullableбез явной проверки на null или пытается передатьnullметоду Java, ожидающему массив примитивного типа, не допускающий null, теперь не скомпилируется.При использовании массива примитивного типа
@NotNullс проверкой на null теперь выдаются предупреждения «Ненужный безопасный вызов» или «Сравнение с null всегда ложно».
Дополнительную информацию см. в соответствующей задаче в YouTrack.
Более строгие правила для абстрактных членов в ожидаемых классах
Что изменилось?
Из-за разделения общих и платформенных исходных файлов при компиляции компилятором K2 мы ввели более строгие правила для абстрактных членов в ожидаемых классах.
В предыдущем компиляторе ожидаемый неабстрактный класс мог наследовать абстрактную функцию, не переопределяя эту функцию. Поскольку компилятор одновременно имел доступ к общему и платформенному коду, он мог проверить, есть ли у абстрактной функции соответствующее переопределение и определение в фактическом классе.
Теперь, когда общие и платформенные исходные файлы компилируются отдельно, унаследованную функцию необходимо явно переопределить в ожидаемом классе, чтобы компилятор знал, что она не является абстрактной. В противном случае компилятор сообщает об ошибке ABSTRACT_MEMBER_NOT_IMPLEMENTED.
Например, предположим, что в общем исходном наборе объявлен абстрактный класс с именем FileSystem, содержащий абстрактную функцию listFiles(). Функция listFiles() определена в платформенном исходном наборе в составе фактического объявления.
Если в общем коде у вас есть ожидаемый неабстрактный класс с именем PlatformFileSystem, наследующий класс FileSystem, класс PlatformFileSystem наследует абстрактную функцию listFiles(). Однако в Kotlin неабстрактный класс не может содержать абстрактную функцию. Чтобы сделать функцию listFiles() неабстрактной, нужно объявить её как переопределение без ключевого слова abstract:
Общий код |
Платформенный код |
|---|---|
abstract class FileSystem {
abstract fun listFiles()
}
expect open class PlatformFileSystem() : FileSystem {
// In Kotlin 2.0.0, an explicit override is needed
expect override fun listFiles()
// Before Kotlin 2.0.0, an override wasn't needed
}
|
actual open class PlatformFileSystem : FileSystem {
actual override fun listFiles() {}
}
|
Как теперь лучше поступать?
Если ожидаемый неабстрактный класс наследует абстрактные функции, добавьте неабстрактное переопределение.
Дополнительную информацию см. в соответствующей задаче в YouTrack.
По тематическим областям
В этих тематических областях перечислены изменения, которые вряд ли повлияют на ваш код, но содержат ссылки на соответствующие задачи YouTrack для дополнительной информации. Изменения, отмеченные звездочкой (*) рядом с идентификатором задачи, описаны в начале раздела.
Вывод типов
Идентификатор задачи |
Название |
|---|---|
Неверный тип в скомпилированной сигнатуре функции для ссылки на свойство, если явно указан тип Normal |
|
Запретить неявный вывод переменной типа в верхнюю границу в контексте вывода типов построителя |
|
K2: требовать явные аргументы типа для вызовов обобщённых аннотаций в литералах массивов |
|
Пропущена проверка подтипов для типа-пересечения |
|
Изменить представление типов на основе параметров типа Java по умолчанию в Kotlin |
|
Изменить выведенный тип префиксного инкремента: возвращать тип результата геттера вместо типа результата оператора inc() |
|
K2: не учитывать наличие @UnsafeVariance при использовании контравариантных параметров |
|
K2: запретить разрешение в пользу перекрытых членов для raw-типов |
|
K2: корректно выводить тип ссылки на вызываемый объект, у которого есть параметр-функция расширения |
|
Согласовать фактический тип переменной деструктуризации с явно указанным типом, если он задан |
|
K2: исправить непоследовательное поведение при переполнении целочисленных литералов |
|
Анонимный тип может быть раскрыт из анонимной функции через аргумент типа |
|
Условие цикла while с break может привести к некорректному смарт-касту |
|
K2: запретить смарт-каст в общем коде для свойства верхнего уровня expect/actual |
|
Операторы инкремента и сложения, изменяющие тип результата, должны влиять на смарт-касты |
|
[LC] K2: явное указание типов переменных в некоторых случаях нарушает смарт-касты с ограничениями, работавшие в K1 |
Обобщения
Идентификатор задачи |
Название |
|---|---|
Устаревание использования синтетического сеттера для проецированного получателя |
|
Запретить переопределение метода Java с параметром raw-типа параметром обобщённого типа |
|
Запретить передачу потенциально допускающего null параметра типа в параметр DNN с проекцией `in` |
|
Устаревание нарушения верхней границы в конструкторах typealias |
|
Исправить некорректность типов для захваченного контравариантного типа на основе класса Java |
|
Запретить некорректный код с собственными верхними границами и захваченными типами |
|
Запретить некорректное нарушение границы во внутреннем обобщённом классе обобщённого внешнего класса |
|
K2: добавить PROJECTION_IN_IMMEDIATE_ARGUMENT_TO_SUPERTYPE для проекций внешних суперклассов внутреннего класса |
|
Сообщать MANY_IMPL_MEMBER_NOT_IMPLEMENTED при наследовании от коллекции примитивов с дополнительной специализированной реализацией из другого суперкласса |
|
K2: запретить вызов конструктора и наследование от псевдонима типа, имеющего модификаторы вариативности в раскрытом типе |
|
Исправить дыру в системе типов, вызванную неправильной обработкой захваченных типов с собственными верхними границами |
|
Запретить обобщённые делегирующие вызовы конструкторов с неверным типом параметра типа |
|
Сообщать о пропущенном нарушении верхней границы, если верхняя граница является захваченным типом |
Разрешение
Идентификатор задачи |
Название |
|---|---|
Обеспечить согласованную работу соглашения invoke с ожидаемым преобразованием синтаксиса |
|
K2: изменить поведение разрешения квалификаторов, когда объект-компаньон имеет приоритет перед статической областью |
|
Сообщать об ошибке неоднозначности при разрешении типов, если импортированы звёздочкой классы с одинаковыми именами |
|
K2: перенести логику разрешения вокруг COMPATIBILITY_WARNING |
|
Ложноотрицательный результат CONFLICTING_INHERITED_MEMBERS, если класс зависимости содержится в двух разных версиях одной зависимости |
|
Свойство invoke функционального типа с получателем имеет приоритет перед функцией расширения invoke |
|
Квалифицированный this: добавить случай this, квалифицированный типом, и повысить его приоритет |
|
Подтвердить неопределённое поведение в случае конфликтов полных имён в classpath |
|
K2: запретить использовать псевдонимы типов в качестве квалификаторов в импортах |
|
K1/K2: некорректная работа башни разрешения для ссылок на типы с неоднозначностью на нижнем уровне |
Видимость
Идентификатор задачи |
Название |
|---|---|
Объявить использование недоступных типов неопределённым поведением |
|
Ложноотрицательный PRIVATE_CLASS_MEMBER_FROM_INLINE при вызове члена объекта-компаньона приватного класса из внутренней inline-функции |
|
Сделать синтетическое свойство невидимым, если эквивалентный геттер невидим, даже когда переопределённое объявление видно |
|
Запретить доступ к сеттеру internal из производного класса в другом модуле |
|
Запретить раскрытие анонимных типов из приватных inline-функций |
|
Запретить неявный доступ к API, не являющемуся публичным, из inline-функции публичного API |
|
Смарт-касты не должны влиять на видимость защищённых членов |
|
Запретить доступ к пропущенным приватным функциям-операторам из публичной inline-функции |
|
K1: сеттер var, переопределяющего protected val, генерируется как public |
|
Запретить переопределение приватными членами на этапе компоновки для Kotlin/Native |
Аннотации
Идентификатор задачи |
Название |
|---|---|
Запретить аннотировать инструкции аннотацией, если у неё нет цели EXPRESSION |
|
Игнорировать выражение в скобках при проверке `REPEATED_ANNOTATION` |
|
K2: запретить аннотации с целевым элементом 'get' на геттерах свойств |
|
Запретить аннотации параметров типа в предложении where |
|
Область видимости компаньона игнорируется при разрешении аннотаций объекта-компаньона |
|
K2: возникла неоднозначность между пользовательскими и обязательными для компилятора аннотациями |
|
Аннотации значений enum не должны копироваться в классы значений enum |
|
K2: `WRONG_ANNOTATION_TARGET` сообщается для несовместимых аннотаций типа, обёрнутого в `()?` |
|
K2: `WRONG_ANNOTATION_TARGET` сообщается для аннотаций типа параметра catch |
Безопасность работы с null
Идентификатор задачи |
Название |
|---|---|
Устаревание небезопасного использования типов массивов, аннотированных как Nullable в Java |
|
K2: изменить семантику вычисления для сочетания безопасных вызовов и операторов соглашений |
|
Порядок суперклассов определяет параметры nullable для унаследованных функций |
|
Сохранять nullable при аппроксимации локальных типов в публичных сигнатурах |
|
Запретить присваивание nullable-значения ненулевому полю Java как вариант небезопасного присваивания |
|
Сообщать об отсутствующих ошибках для nullable-аргументов уровня ошибки типов Java уровня предупреждения |
Взаимодействие с Java
Идентификатор задачи |
Название |
|---|---|
Запретить наличие в исходном коде классов Java и Kotlin с одинаковыми полными именами |
|
Поведение классов, унаследованных от коллекций Java, непоследовательно и зависит от порядка суперклассов |
|
K2: неопределённое поведение в случае наследования класса Java от приватного класса Kotlin |
|
Передача метода Java с vararg в inline-функцию приводит во время выполнения к массиву массивов вместо обычного массива |
|
Разрешить переопределение членов internal в иерархии K-J-K |
Свойства
Идентификатор задачи |
Название |
|---|---|
[LC] Запретить отложенную инициализацию открытых свойств с полем хранения |
|
Устаревание пропущенного MUST_BE_INITIALIZED, если первичный конструктор отсутствует или класс является локальным |
|
Запретить рекурсивное разрешение в случае потенциальных вызовов invoke для свойств |
|
Устаревание смарт-каста свойства базового класса из невидимого производного класса, если базовый класс находится в другом модуле |
|
K2: пропущена OPT_IN_USAGE_ERROR для свойств класса данных |
Поток управления
Идентификатор задачи |
Название |
|---|---|
Непоследовательные правила CFA в блоке инициализации класса в K1 и K2 |
|
Несоответствие K1/K2 для условного выражения if без ветви else в скобках |
|
Ложноотрицательный результат "VAL_REASSIGNMENT" в блоке try/catch с инициализацией в функции области видимости |
|
Передавать информацию о потоке данных из блока try в блоки catch и finally |
Классы enum
Идентификатор задачи |
Название |
|---|---|
Запретить доступ к объекту-компаньону класса enum во время инициализации элемента enum |
|
Сообщать о пропущенной ошибке для виртуального inline-метода в классах enum |
|
Сообщать о неоднозначности при разрешении между свойством/полем и элементом enum |
|
Изменить поведение разрешения квалификаторов, когда свойство компаньона имеет приоритет перед элементом enum |
Функциональные (SAM) интерфейсы
Идентификатор задачи |
Название |
|---|---|
Устаревание вызовов SAM-конструкторов, требующих OptIn без аннотации |
|
Запрет возврата значений с неверной nullability из лямбды для SAM-конструктора функциональных интерфейсов JDK |
|
SAM-преобразование типов параметров ссылок на вызываемые объекты приводит к CCE |
Разное
Идентификатор задачи |
Название |
|---|---|
K2/MPP сообщает [ABSTRACT_MEMBER_NOT_IMPLEMENTED] для наследника в общем коде, если реализация находится в фактическом аналоге |
|
Изменить поведение квалифицированного this при возможных конфликтах меток |
|
Исправить некорректное переименование функций в бэкенде JVM при случайном конфликте перегрузок в подклассе Java |
|
[Проблема LC] Запретить объявления анонимных функций с модификатором suspend в позициях операторов |
|
OptIn: запретить вызовы конструкторов с аргументами по умолчанию (параметрами со значениями по умолчанию) под маркером |
|
Преобразование в Unit случайно разрешено для выражений с переменными и разрешением invoke |
|
Запретить преобразование ссылок на вызываемые объекты с адаптациями в KFunction |
|
[LC] K2 нарушает работу `false && ...` и `false || ...` |
|
[LC] Устаревание ключевых слов `header`/`impl` |
|
Генерировать все лямбды Kotlin по умолчанию с помощью invokedynamic + LambdaMetafactory |
Совместимость с выпусками Kotlin
Поддержка нового компилятора K2 реализована в следующих выпусках Kotlin:
Выпуск Kotlin |
Уровень стабильности |
|---|---|
2.0.0–2.4.20 |
Стабильный |
1.9.20–1.9.25 |
Бета |
1.9.0–1.9.10 |
JVM — бета |
1.7.0–1.8.22 |
Альфа |
Совместимость с библиотеками Kotlin
Если вы работаете с Kotlin/JVM, компилятор K2 работает с библиотеками, скомпилированными с помощью любой версии Kotlin.
Если вы работаете с Kotlin Multiplatform, гарантируется работа компилятора K2 с библиотеками, скомпилированными с помощью Kotlin версии 1.9.20 и новее.
Поддержка плагинов компилятора
В настоящее время компилятор Kotlin K2 поддерживает следующие плагины компилятора Kotlin:
Кроме того, компилятор Kotlin K2 поддерживает:
Плагин компилятора Jetpack Compose версии 1.5.0 и новее.
Обработка символов Kotlin (KSP) начиная с версии KSP2.
Обновление собственных плагинов компилятора
Процесс обновления зависит от типа вашего собственного плагина и может выполняться двумя способами.
Плагины компилятора только для бэкенда
Если ваш плагин реализует только точки расширения IrGenerationExtension, процесс ничем не отличается от обновления для любого другого нового выпуска компилятора. Проверьте, изменился ли используемый вами API, и при необходимости внесите соответствующие изменения.
Плагины компилятора для бэкенда и фронтенда
Если ваш плагин использует точки расширения, связанные с фронтендом, необходимо переписать его с помощью нового API компилятора K2. Введение в новый API см. в разделе API плагинов FIR.
Поделитесь отзывом о новом компиляторе K2
Мы будем рады любым вашим отзывам!
Сообщайте о любых проблемах, с которыми вы столкнулись при переходе на новый компилятор K2, в нашем трекере задач.
Включите параметр «Отправлять статистику использования», чтобы разрешить JetBrains собирать анонимные данные об использовании K2.
© 2010–2026 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/k2-compiler-migration-guide.html