Avatar ·

В чем разница между const и val в Kotlin? Когда что использовать?

Привет! Я начал изучать Kotlin и никак не могу понять разницу между `const` и `val`. Вроде бы оба объявляют неизменяемые значения, но компилятор ругается, когда я пытаюсь использовать `const` внутри функции или с какими-то выражениями. Вот пример, который меня смущает: ```kotlin // Это работает val name = "John" val currentTime = System.currentTimeMillis() // А это не работает - ошибка компиляции const val name2 = "John" const val currentTime2 = System.currentTimeMillis() // Ошибка! ``` Почему `const val currentTime2` не компилируется? Ведь `val` тоже неизменяемый. И еще я заметил, что `const` можно использовать только на уровне файла или внутри `object`, а `val` — где угодно. В чем тут глубинная разница? Когда стоит использовать `const`, а когда просто `val`? Объясните, пожалуйста, на примерах. Буду очень благодарен! <<>>

Главное различие: время вычисления и способ хранения

const и val оба создают неизменяемые (read-only) переменные, но ключевое отличие — когда значение вычисляется и как оно хранится.

  • val — это runtime-константа. Значение вычисляется один раз при первом обращении (или при инициализации объекта) и хранится в памяти. Его можно вычислить из любого выражения, включая вызовы функций и свойства других объектов.
  • const val — это compile-time константа. Значение должно быть известно на этапе компиляции. Компилятор подставляет это значение напрямую в код (как в Java static final примитивов и строк).

Правила для const val

  1. Должно быть объявлено на уровне файла (вне класса/функции) или внутри object (включая companion object).
  2. Значение должно быть примитивом (Int, Double, Boolean и т.д.) или строкой (String).
  3. Нельзя использовать вызовы функций, конструкторы или выражения, которые вычисляются во время выполнения.

Примеры, которые прояснят разницу

1. Простые литералы — работают оба варианта:

const val APP_NAME = "MyApp"     // Компилятор подставит "MyApp" везде
val appName = "MyApp"            // То же самое, но через runtime-ссылку

2. Вызов функции — работает только val:

val currentTime = System.currentTimeMillis() // Ок: вычисляется при инициализации
// const val currentTime2 = System.currentTimeMillis() // Ошибка! Неизвестно на этапе компиляции

3. Использование в аннотациях — только const:

@Retention(AnnotationRetention.RUNTIME)
annotation class MyAnnotation {
    companion object {
        const val TAG = "Debug"  // Можно использовать в аннотациях
        // val TAG = "Debug"     // Ошибка! Аннотации требуют compile-time констант
    }
}

@MyAnnotation(TAG = MyAnnotation.TAG) // Работает только с const
fun test() {}

4. Производительность и встраивание:

// Файл Config.kt
const val TIMEOUT = 5000

// Файл Network.kt
fun connect() {
    Thread.sleep(TIMEOUT) // Компилятор заменит на Thread.sleep(5000)
    // Если бы было val, то было бы обращение к ссылке Config.TIMEOUT
}

Когда что использовать?

  • const val — для "настоящих" констант: строковые идентификаторы, числовые параметры (таймауты, размеры), которые точно не изменятся между билдами. Используйте, если значение нужно в аннотациях или в when с when (value) { CONSTANT -> ... }.
  • val — для всего остального: любые вычисляемые значения, объекты, списки, результат работы функций. val гарантирует, что ссылка не изменится, но само значение может быть сложным объектом.

Важно: const val не делает объект иммутабельным. Если вы напишете const val list = listOf(1,2,3) — это ошибка компиляции, потому что список — не примитив и не строка. Для неизменяемых коллекций используйте val + listOf.

В итоге: const — это для значений, которые компилятор может "зашить" в байт-код, а val — для обычных неизменяемых ссылок.

<<>>

Дополнение: const val и companion object

Коллега правильно описал техническую разницу. Хочу добавить важный нюанс про использование const в companion object — это самый частый кейс в реальных проектах.

Когда вы пишете:

class User {
    companion object {
        const val MAX_AGE = 150
    }
}

Это эквивалент Java public static final int MAX_AGE = 150;. К нему можно обращаться как User.MAX_AGE.

Если же использовать val внутри companion object:

class User {
    companion object {
        val maxAge = 150 // Это не static final, а обычное поле экземпляра companion object
    }
}

Доступ будет через User.Companion.getMaxAge() — то есть лишний вызов геттера. На производительность это влияет редко, но для критичных мест (например, в циклах или аннотациях) лучше использовать const.

Ещё один практический совет: если вы пишете библиотеку и хотите, чтобы константы были доступны из Java-кода как static final, используйте const val. Без const из Java придётся обращаться через ClassName.Companion.getField(), что неудобно.

И последнее: не пытайтесь сделать const val для сложных объектов вроде Regex или SimpleDateFormat — это вызовет ошибку компиляции. Для таких случаев используйте val с ленивой инициализацией (by lazy).